Bug 1460811 - migrate XULStore to rkv r=bgrins,lina
authorMyk Melez <myk@mykzilla.org>
Mon, 22 Apr 2019 02:59:51 +0000
changeset 470328 76a363f06ebd40d418d483b41b12721ab728f38c
parent 470327 593761a810cea50aebb2b9a423af791c773c10ac
child 470329 6cbc4908bc4bb8e260cb452d36d5a1c9abf4468f
push id112863
push usershindli@mozilla.com
push dateMon, 22 Apr 2019 09:53:25 +0000
treeherdermozilla-inbound@ab1da7fa2ad0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbgrins, lina
bugs1460811
milestone68.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1460811 - migrate XULStore to rkv r=bgrins,lina Differential Revision: https://phabricator.services.mozilla.com/D25355
Cargo.lock
dom/xul/XULPersist.cpp
dom/xul/XULPersist.h
testing/talos/talos/xtalos/xperf_whitelist.json
toolkit/components/moz.build
toolkit/components/xulstore/Cargo.toml
toolkit/components/xulstore/XULStore.cpp
toolkit/components/xulstore/XULStore.h
toolkit/components/xulstore/XULStore.jsm
toolkit/components/xulstore/components.conf
toolkit/components/xulstore/moz.build
toolkit/components/xulstore/nsIXULStore.idl
toolkit/components/xulstore/src/error.rs
toolkit/components/xulstore/src/ffi.rs
toolkit/components/xulstore/src/iter.rs
toolkit/components/xulstore/src/lib.rs
toolkit/components/xulstore/src/persist.rs
toolkit/components/xulstore/src/statics.rs
toolkit/components/xulstore/tests/chrome/window_persistence.xul
toolkit/components/xulstore/tests/gtest/Cargo.toml
toolkit/components/xulstore/tests/gtest/TestXULStore.cpp
toolkit/components/xulstore/tests/gtest/moz.build
toolkit/components/xulstore/tests/xpcshell/test_XULStore_migration.js
toolkit/components/xulstore/tests/xpcshell/test_XULStore_migration_fail_invalid_data.js
toolkit/components/xulstore/tests/xpcshell/test_XULStore_migration_fail_invalid_json.js
toolkit/components/xulstore/tests/xpcshell/test_XULStore_migration_profile_change.js
toolkit/components/xulstore/tests/xpcshell/xpcshell.ini
toolkit/library/rust/shared/Cargo.toml
toolkit/library/rust/shared/lib.rs
toolkit/modules/Services.jsm
xpcom/build/Services.py
xpcom/rust/nserror/src/lib.rs
xpfe/appshell/nsXULWindow.cpp
xpfe/appshell/nsXULWindow.h
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1243,16 +1243,17 @@ dependencies = [
  "prefs_parser 0.0.1",
  "profiler_helper 0.1.0",
  "rsdparsa_capi 0.1.0",
  "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
  "storage 0.1.0",
  "u2fhid 0.2.3",
  "webrender_bindings 0.1.0",
  "xpcom 0.1.0",
+ "xulstore 0.1.0",
 ]
 
 [[package]]
 name = "gkrust_utils"
 version = "0.1.0"
 dependencies = [
  "nsstring 0.1.0",
  "uuid 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -3428,16 +3429,34 @@ name = "xpcom_macros"
 version = "0.1.0"
 dependencies = [
  "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "quote 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "syn 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
+name = "xulstore"
+version = "0.1.0"
+dependencies = [
+ "crossbeam-utils 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "failure 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)",
+ "lmdb-rkv 0.11.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "moz_task 0.1.0",
+ "nserror 0.1.0",
+ "nsstring 0.1.0",
+ "rkv 0.9.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde_json 1.0.26 (registry+https://github.com/rust-lang/crates.io-index)",
+ "xpcom 0.1.0",
+]
+
+[[package]]
 name = "yaml-rust"
 version = "0.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "linked-hash-map 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
--- a/dom/xul/XULPersist.cpp
+++ b/dom/xul/XULPersist.cpp
@@ -1,17 +1,16 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
 
 #include "XULPersist.h"
-
-#include "nsIXULStore.h"
+#include "mozilla/XULStore.h"
 
 namespace mozilla {
 namespace dom {
 
 static bool ShouldPersistAttribute(Element* aElement, nsAtom* aAttribute) {
   if (aElement->IsXULElement(nsGkAtoms::window)) {
     // This is not an element of the top document, its owner is
     // not an nsXULWindow. Persist it.
@@ -75,110 +74,95 @@ void XULPersist::Persist(Element* aEleme
   if (!mDocument) {
     return;
   }
   // For non-chrome documents, persistance is simply broken
   if (!nsContentUtils::IsSystemPrincipal(mDocument->NodePrincipal())) {
     return;
   }
 
-  if (!mLocalStore) {
-    mLocalStore = do_GetService("@mozilla.org/xul/xulstore;1");
-    if (NS_WARN_IF(!mLocalStore)) {
-      return;
-    }
-  }
-
   nsAutoString id;
 
   aElement->GetAttr(kNameSpaceID_None, nsGkAtoms::id, id);
   nsAtomString attrstr(aAttribute);
 
   nsAutoString valuestr;
   aElement->GetAttr(kNameSpaceID_None, aAttribute, valuestr);
 
   nsAutoCString utf8uri;
   nsresult rv = mDocument->GetDocumentURI()->GetSpec(utf8uri);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return;
   }
   NS_ConvertUTF8toUTF16 uri(utf8uri);
 
   bool hasAttr;
-  rv = mLocalStore->HasValue(uri, id, attrstr, &hasAttr);
+  rv = XULStore::HasValue(uri, id, attrstr, hasAttr);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return;
   }
 
   if (hasAttr && valuestr.IsEmpty()) {
-    mLocalStore->RemoveValue(uri, id, attrstr);
+    rv = XULStore::RemoveValue(uri, id, attrstr);
+    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "value removed");
     return;
   }
 
   // Persisting attributes to top level windows is handled by nsXULWindow.
   if (aElement->IsXULElement(nsGkAtoms::window)) {
     if (nsCOMPtr<nsIXULWindow> win =
             mDocument->GetXULWindowIfToplevelChrome()) {
       return;
     }
   }
 
-  mLocalStore->SetValue(uri, id, attrstr, valuestr);
+  rv = XULStore::SetValue(uri, id, attrstr, valuestr);
+  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "value set");
 }
 
 nsresult XULPersist::ApplyPersistentAttributes() {
   if (!mDocument) {
     return NS_ERROR_NOT_AVAILABLE;
   }
   // For non-chrome documents, persistance is simply broken
   if (!nsContentUtils::IsSystemPrincipal(mDocument->NodePrincipal())) {
     return NS_ERROR_NOT_AVAILABLE;
   }
 
   // Add all of the 'persisted' attributes into the content
   // model.
-  if (!mLocalStore) {
-    mLocalStore = do_GetService("@mozilla.org/xul/xulstore;1");
-    if (NS_WARN_IF(!mLocalStore)) {
-      return NS_ERROR_NOT_INITIALIZED;
-    }
-  }
-
   ApplyPersistentAttributesInternal();
 
   return NS_OK;
 }
 
 nsresult XULPersist::ApplyPersistentAttributesInternal() {
   nsCOMArray<Element> elements;
 
   nsAutoCString utf8uri;
   nsresult rv = mDocument->GetDocumentURI()->GetSpec(utf8uri);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
   NS_ConvertUTF8toUTF16 uri(utf8uri);
 
   // Get a list of element IDs for which persisted values are available
-  nsCOMPtr<nsIStringEnumerator> ids;
-  rv = mLocalStore->GetIDsEnumerator(uri, getter_AddRefs(ids));
+  UniquePtr<XULStoreIterator> ids;
+  rv = XULStore::GetIDs(uri, ids);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
-  while (1) {
-    bool hasmore = false;
-    ids->HasMore(&hasmore);
-    if (!hasmore) {
-      break;
+  while (ids->HasMore()) {
+    nsAutoString id;
+    rv = ids->GetNext(&id);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
     }
 
-    nsAutoString id;
-    ids->GetNext(id);
-
     // We want to hold strong refs to the elements while applying
     // persistent attributes, just in case.
     const nsTArray<Element*>* allElements = mDocument->GetAllElementsForId(id);
     if (!allElements) {
       continue;
     }
     elements.Clear();
     elements.SetCapacity(allElements->Length());
@@ -200,34 +184,31 @@ nsresult XULPersist::ApplyPersistentAttr
   nsAutoCString utf8uri;
   nsresult rv = mDocument->GetDocumentURI()->GetSpec(utf8uri);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
   NS_ConvertUTF8toUTF16 uri(utf8uri);
 
   // Get a list of attributes for which persisted values are available
-  nsCOMPtr<nsIStringEnumerator> attrs;
-  rv = mLocalStore->GetAttributeEnumerator(uri, aID, getter_AddRefs(attrs));
+  UniquePtr<XULStoreIterator> attrs;
+  rv = XULStore::GetAttrs(uri, aID, attrs);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
-  while (1) {
-    bool hasmore = PR_FALSE;
-    attrs->HasMore(&hasmore);
-    if (!hasmore) {
-      break;
+  while (attrs->HasMore()) {
+    nsAutoString attrstr;
+    rv = attrs->GetNext(&attrstr);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
     }
 
-    nsAutoString attrstr;
-    attrs->GetNext(attrstr);
-
     nsAutoString value;
-    rv = mLocalStore->GetValue(uri, aID, attrstr, value);
+    rv = XULStore::GetValue(uri, aID, attrstr, value);
     if (NS_WARN_IF(NS_FAILED(rv))) {
       return rv;
     }
 
     RefPtr<nsAtom> attr = NS_Atomize(attrstr);
     if (NS_WARN_IF(!attr)) {
       return NS_ERROR_OUT_OF_MEMORY;
     }
--- a/dom/xul/XULPersist.h
+++ b/dom/xul/XULPersist.h
@@ -2,18 +2,16 @@
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef mozilla_dom_XULPersist_h
 #define mozilla_dom_XULPersist_h
 
-class nsIXULStore;
-
 namespace mozilla {
 namespace dom {
 
 class XULPersist final : public nsStubDocumentObserver {
  public:
   NS_DECL_ISUPPORTS
 
   explicit XULPersist(Document* aDocument);
@@ -28,17 +26,16 @@ class XULPersist final : public nsStubDo
 
  private:
   ~XULPersist();
   nsresult ApplyPersistentAttributes();
   nsresult ApplyPersistentAttributesInternal();
   nsresult ApplyPersistentAttributesToElements(const nsAString& aID,
                                                nsCOMArray<Element>& aElements);
 
-  nsCOMPtr<nsIXULStore> mLocalStore;
   // A weak pointer to our document. Nulled out by DropDocumentReference.
   Document* MOZ_NON_OWNING_REF mDocument;
 };
 
 }  // namespace dom
 }  // namespace mozilla
 
 #endif  // mozilla_dom_XULPersist_h
--- a/testing/talos/talos/xtalos/xperf_whitelist.json
+++ b/testing/talos/talos/xtalos/xperf_whitelist.json
@@ -561,21 +561,21 @@
     "maxbytes": 512
   },
   "{profile}\\user.js": {
     "mincount": 4,
     "maxcount": 4,
     "minbytes": 6000,
     "maxbytes": 6000
   },
-  "{profile}\\xulstore.json": {
-    "mincount": 0,
-    "maxcount": 0,
-    "minbytes": 0,
-    "maxbytes": 702
-  },
   "{talos}\\talos\\tests\\{tp5n_files}": {
     "mincount": 0,
     "maxcount": 2,
     "minbytes": 0,
     "maxbytes": 16384
+  },
+  "{profile}\\xulstore\\data.mdb": {
+    "mincount": 0,
+    "maxcount": 4,
+    "minbytes": 0,
+    "maxbytes": 608
   }
 }
--- a/toolkit/components/moz.build
+++ b/toolkit/components/moz.build
@@ -75,17 +75,17 @@ DIRS += [
     'utils',
     'url-classifier',
     'urlformatter',
     'viewconfig',
     'viewsource',
     'windowcreator',
     'windowwatcher',
     'workerloader',
-    'xulstore'
+    'xulstore',
 ]
 
 if CONFIG['MOZ_BUILD_APP'] != 'mobile/android':
     DIRS += ['narrate'];
 
     if CONFIG['NS_PRINTING']:
         DIRS += ['printing']
 
new file mode 100644
--- /dev/null
+++ b/toolkit/components/xulstore/Cargo.toml
@@ -0,0 +1,25 @@
+[package]
+name = "xulstore"
+version = "0.1.0"
+authors = ["nobody@mozilla.org"]
+license = "MPL-2.0"
+
+[dependencies]
+crossbeam-utils = "0.6.3"
+lazy_static = "1.0"
+libc = "0.2"
+lmdb-rkv = "0.11.2"
+log = "0.4"
+moz_task = { path = "../../../xpcom/rust/moz_task" }
+nsstring = { path = "../../../xpcom/rust/nsstring" }
+nserror = { path = "../../../xpcom/rust/nserror" }
+rkv = "0.9.3"
+serde_json = "1"
+xpcom = { path = "../../../xpcom/rust/xpcom" }
+
+# Get rid of failure's dependency on backtrace. Eventually
+# backtrace will move into Rust core, but we don't need it here.
+[dependencies.failure]
+version = "0.1"
+default_features = false
+features = ["derive"]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/xulstore/XULStore.cpp
@@ -0,0 +1,107 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/XULStore.h"
+#include "nsCOMPtr.h"
+#include "nsIXULStore.h"
+
+namespace mozilla {
+
+// The XULStore API is implemented in Rust and exposed to C++ via a set of
+// C functions with the "xulstore_" prefix.  We declare them in this anonymous
+// namespace to prevent C++ code outside this file from accessing them,
+// as they are an internal implementation detail, and C++ code should use
+// the mozilla::XULStore::* functions and mozilla::XULStoreIterator class
+// declared in XULStore.h.
+namespace {
+extern "C" {
+void xulstore_new_service(nsIXULStore** result);
+nsresult xulstore_set_value(const nsAString* doc, const nsAString* id,
+                            const nsAString* attr, const nsAString* value);
+nsresult xulstore_has_value(const nsAString* doc, const nsAString* id,
+                            const nsAString* attr, bool* has_value);
+nsresult xulstore_get_value(const nsAString* doc, const nsAString* id,
+                            const nsAString* attr, nsAString* value);
+nsresult xulstore_remove_value(const nsAString* doc, const nsAString* id,
+                               const nsAString* attr);
+XULStoreIterator* xulstore_get_ids(const nsAString* doc, nsresult* result);
+XULStoreIterator* xulstore_get_attrs(const nsAString* doc, const nsAString* id,
+                                     nsresult* result);
+bool xulstore_iter_has_more(const XULStoreIterator*);
+nsresult xulstore_iter_get_next(XULStoreIterator*, nsAString* value);
+void xulstore_iter_free(XULStoreIterator* iterator);
+}
+
+// A static reference to the nsIXULStore singleton that JS uses to access
+// the store.  Retrieved via mozilla::XULStore::GetService().
+static StaticRefPtr<nsIXULStore> sXULStore;
+}  // namespace
+
+bool XULStoreIterator::HasMore() const { return xulstore_iter_has_more(this); }
+
+nsresult XULStoreIterator::GetNext(nsAString* item) {
+  return xulstore_iter_get_next(this, item);
+}
+
+void DefaultDelete<XULStoreIterator>::operator()(XULStoreIterator* ptr) const {
+  xulstore_iter_free(ptr);
+}
+
+namespace XULStore {
+already_AddRefed<nsIXULStore> GetService() {
+  nsCOMPtr<nsIXULStore> xulStore;
+
+  if (sXULStore) {
+    xulStore = sXULStore;
+  } else {
+    xulstore_new_service(getter_AddRefs(xulStore));
+    sXULStore = xulStore;
+    mozilla::ClearOnShutdown(&sXULStore);
+  }
+
+  return xulStore.forget();
+}
+
+nsresult SetValue(const nsAString& doc, const nsAString& id,
+                  const nsAString& attr, const nsAString& value) {
+  return xulstore_set_value(&doc, &id, &attr, &value);
+}
+nsresult HasValue(const nsAString& doc, const nsAString& id,
+                  const nsAString& attr, bool& has_value) {
+  return xulstore_has_value(&doc, &id, &attr, &has_value);
+}
+nsresult GetValue(const nsAString& doc, const nsAString& id,
+                  const nsAString& attr, nsAString& value) {
+  return xulstore_get_value(&doc, &id, &attr, &value);
+}
+nsresult RemoveValue(const nsAString& doc, const nsAString& id,
+                     const nsAString& attr) {
+  return xulstore_remove_value(&doc, &id, &attr);
+}
+nsresult GetIDs(const nsAString& doc, UniquePtr<XULStoreIterator>& iter) {
+  // We assign the value of the iter here in C++ via a return value
+  // rather than in the Rust function via an out parameter in order
+  // to ensure that any old value is deleted, since the UniquePtr's
+  // assignment operator won't delete the old value if the assignment
+  // happens in Rust.
+  nsresult result;
+  iter.reset(xulstore_get_ids(&doc, &result));
+  return result;
+}
+nsresult GetAttrs(const nsAString& doc, const nsAString& id,
+                  UniquePtr<XULStoreIterator>& iter) {
+  // We assign the value of the iter here in C++ via a return value
+  // rather than in the Rust function via an out parameter in order
+  // to ensure that any old value is deleted, since the UniquePtr's
+  // assignment operator won't delete the old value if the assignment
+  // happens in Rust.
+  nsresult result;
+  iter.reset(xulstore_get_attrs(&doc, &id, &result));
+  return result;
+}
+
+};  // namespace XULStore
+};  // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/toolkit/components/xulstore/XULStore.h
@@ -0,0 +1,55 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+/*
+ * This file declares the XULStore API for C++ via the mozilla::XULStore
+ * namespace and the mozilla::XULStoreIterator class.  It also declares
+ * the mozilla::XULStore::GetService() function that the component manager
+ * uses to instantiate and retrieve the nsIXULStore singleton.
+ */
+
+#ifndef mozilla_XULStore_h
+#define mozilla_XULStore_h
+
+#include "nsIXULStore.h"
+
+namespace mozilla {
+class XULStoreIterator final {
+ public:
+  bool HasMore() const;
+  nsresult GetNext(nsAString* item);
+
+ private:
+  XULStoreIterator() = delete;
+  XULStoreIterator(const XULStoreIterator&) = delete;
+  XULStoreIterator& operator=(const XULStoreIterator&) = delete;
+  ~XULStoreIterator() = delete;
+};
+
+template <>
+class DefaultDelete<XULStoreIterator> {
+ public:
+  void operator()(XULStoreIterator* ptr) const;
+};
+
+namespace XULStore {
+// Instantiates and retrieves the nsIXULStore singleton that JS uses to access
+// the store.  C++ code should use the mozilla::XULStore::* functions instead.
+already_AddRefed<nsIXULStore> GetService();
+
+nsresult SetValue(const nsAString& doc, const nsAString& id,
+                  const nsAString& attr, const nsAString& value);
+nsresult HasValue(const nsAString& doc, const nsAString& id,
+                  const nsAString& attr, bool& has_value);
+nsresult GetValue(const nsAString& doc, const nsAString& id,
+                  const nsAString& attr, nsAString& value);
+nsresult RemoveValue(const nsAString& doc, const nsAString& id,
+                     const nsAString& attr);
+nsresult GetIDs(const nsAString& doc, UniquePtr<XULStoreIterator>& iter);
+nsresult GetAttrs(const nsAString& doc, const nsAString& id,
+                  UniquePtr<XULStoreIterator>& iter);
+};  // namespace XULStore
+};  // namespace mozilla
+
+#endif  // mozilla_XULStore_h
--- a/toolkit/components/xulstore/XULStore.jsm
+++ b/toolkit/components/xulstore/XULStore.jsm
@@ -1,308 +1,95 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-// Enables logging and shorter save intervals.
+"use strict";
+
+// This JS module wraps the nsIXULStore XPCOM service with useful abstractions.
+// In particular, it wraps the enumerators returned by getIDsEnumerator()
+// and getAttributeEnumerator() in JS objects that implement the iterable
+// protocol.  It also implements the persist() method.  JS consumers should use
+// this module rather than accessing nsIXULStore directly.
+
+const EXPORTED_SYMBOLS = ["XULStore"];
+
+const xulStore = Cc["@mozilla.org/xul/xulstore;1"].getService(Ci.nsIXULStore);
+
+// Enables logging.
 const debugMode = false;
 
-// Delay when a change is made to when the file is saved.
-// 30 seconds normally, or 3 seconds for testing
-const WRITE_DELAY_MS = (debugMode ? 3 : 30) * 1000;
-
-const XULSTORE_CID = Components.ID("{6f46b6f4-c8b1-4bd4-a4fa-9ebbed0753ea}");
-const STOREDB_FILENAME = "xulstore.json";
-
-const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
-const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
-
-ChromeUtils.defineModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
-
-function XULStore() {
-  if (!Services.appinfo.inSafeMode)
-    this.load();
+// Internal function for logging debug messages to the Error Console window
+function log(message) {
+  if (!debugMode)
+    return;
+  console.log("XULStore: " + message);
 }
 
-XULStore.prototype = {
-  classID: XULSTORE_CID,
-  QueryInterface: ChromeUtils.generateQI([Ci.nsIObserver, Ci.nsIXULStore,
-                                          Ci.nsISupportsWeakReference]),
-  _xpcom_factory: XPCOMUtils.generateSingletonFactory(XULStore),
-
-  /* ---------- private members ---------- */
-
-  /*
-   * The format of _data is _data[docuri][elementid][attribute]. For example:
-   *  {
-   *      "chrome://blah/foo.xul" : {
-   *                                    "main-window" : { aaa : 1, bbb : "c" },
-   *                                    "barColumn"   : { ddd : 9, eee : "f" },
-   *                                },
-   *
-   *      "chrome://foopy/b.xul" :  { ... },
-   *      ...
-   *  }
-   */
-  _data: {},
-  _storeFile: null,
-  _needsSaving: false,
-  _saveAllowed: true,
-  _writeTimer: Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer),
-
-  load() {
-    Services.obs.addObserver(this, "profile-before-change", true);
-
-    try {
-      this._storeFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
-    } catch (ex) {
-      try {
-        this._storeFile = Services.dirsvc.get("ProfDS", Ci.nsIFile);
-      } catch (ex) {
-        throw new Error("Can't find profile directory.");
-      }
-    }
-    this._storeFile.append(STOREDB_FILENAME);
-
-    this.readFile();
-  },
-
-  observe(subject, topic, data) {
-    this.writeFile();
-    if (topic == "profile-before-change") {
-      this._saveAllowed = false;
-    }
-  },
+const XULStore = {
+  setValue: xulStore.setValue,
+  hasValue: xulStore.hasValue,
+  getValue: xulStore.getValue,
+  removeValue: xulStore.removeValue,
+  removeDocument: xulStore.removeDocument,
 
-  /*
-   * Internal function for logging debug messages to the Error Console window
+  /**
+   * Sets a value for a specified node's attribute, except in
+   * the case below (following the original XULDocument::persist):
+   * If the value is empty and if calling `hasValue` with the node's
+   * document and ID and `attr` would return true, then the
+   * value instead gets removed from the store (see Bug 1476680).
+   *
+   * @param node - DOM node
+   * @param attr - attribute to store
    */
-  log(message) {
-    if (!debugMode)
-      return;
-    console.log("XULStore: " + message);
-  },
-
-  readFile() {
-    try {
-      this._data = JSON.parse(Cu.readUTF8File(this._storeFile));
-    } catch (e) {
-      this.log("Error reading JSON: " + e);
-      // This exception could mean that the file didn't exist.
-      // We'll just ignore the error and start with a blank slate.
-    }
-  },
-
-  async writeFile() {
-    if (!this._needsSaving)
-      return;
-
-    this._needsSaving = false;
-
-    this.log("Writing to xulstore.json");
-
-    try {
-      let data = JSON.stringify(this._data);
-      let encoder = new TextEncoder();
-
-      data = encoder.encode(data);
-      await OS.File.writeAtomic(this._storeFile.path, data,
-                              { tmpPath: this._storeFile.path + ".tmp" });
-    } catch (e) {
-      this.log("Failed to write xulstore.json: " + e);
-      throw e;
-    }
-  },
-
-  markAsChanged() {
-    if (this._needsSaving || !this._storeFile)
-      return;
-
-    // Don't write the file more than once every 30 seconds.
-    this._needsSaving = true;
-    this._writeTimer.init(this, WRITE_DELAY_MS, Ci.nsITimer.TYPE_ONE_SHOT);
-  },
-
-  /* ---------- interface implementation ---------- */
-
   persist(node, attr) {
     if (!node.id) {
       throw new Error("Node without ID passed into persist()");
     }
 
     const uri = node.ownerDocument.documentURI;
     const value = node.getAttribute(attr);
 
     if (node.localName == "window") {
-      this.log("Persisting attributes to windows is handled by nsXULWindow.");
+      log("Persisting attributes to windows is handled by nsXULWindow.");
       return;
     }
 
     // See Bug 1476680 - we could drop the `hasValue` check so that
     // any time there's an empty attribute it gets removed from the
     // store. Since this is copying behavior from document.persist,
     // callers would need to be updated with that change.
-    if (!value && this.hasValue(uri, node.id, attr)) {
-      this.removeValue(uri, node.id, attr);
+    if (!value && xulStore.hasValue(uri, node.id, attr)) {
+      xulStore.removeValue(uri, node.id, attr);
     } else {
-      this.setValue(uri, node.id, attr, value);
-    }
-  },
-
-  setValue(docURI, id, attr, value) {
-    this.log("Saving " + attr + "=" + value + " for id=" + id + ", doc=" + docURI);
-
-    if (!this._saveAllowed) {
-      Services.console.logStringMessage("XULStore: Changes after profile-before-change are ignored!");
-      return;
-    }
-
-    // bug 319846 -- don't save really long attributes or values.
-    if (id.length > 512 || attr.length > 512) {
-      throw Components.Exception("id or attribute name too long", Cr.NS_ERROR_ILLEGAL_VALUE);
-    }
-
-    if (value.length > 4096) {
-      Services.console.logStringMessage("XULStore: Warning, truncating long attribute value");
-      value = value.substr(0, 4096);
-    }
-
-    let obj = this._data;
-    if (!(docURI in obj)) {
-      obj[docURI] = {};
-    }
-    obj = obj[docURI];
-    if (!(id in obj)) {
-      obj[id] = {};
-    }
-    obj = obj[id];
-
-    // Don't set the value if it is already set to avoid saving the file.
-    if (attr in obj && obj[attr] == value)
-      return;
-
-    obj[attr] = value; // IE, this._data[docURI][id][attr] = value;
-
-    this.markAsChanged();
-  },
-
-  hasValue(docURI, id, attr) {
-    this.log("has store value for id=" + id + ", attr=" + attr + ", doc=" + docURI);
-
-    let ids = this._data[docURI];
-    if (ids) {
-      let attrs = ids[id];
-      if (attrs) {
-        return attr in attrs;
-      }
-    }
-
-    return false;
-  },
-
-  getValue(docURI, id, attr) {
-    this.log("get store value for id=" + id + ", attr=" + attr + ", doc=" + docURI);
-
-    let ids = this._data[docURI];
-    if (ids) {
-      let attrs = ids[id];
-      if (attrs) {
-        return attrs[attr] || "";
-      }
-    }
-
-    return "";
-  },
-
-  removeValue(docURI, id, attr) {
-    this.log("remove store value for id=" + id + ", attr=" + attr + ", doc=" + docURI);
-
-    if (!this._saveAllowed) {
-      Services.console.logStringMessage("XULStore: Changes after profile-before-change are ignored!");
-      return;
-    }
-
-    let ids = this._data[docURI];
-    if (ids) {
-      let attrs = ids[id];
-      if (attrs && attr in attrs) {
-        delete attrs[attr];
-
-        if (Object.getOwnPropertyNames(attrs).length == 0) {
-          delete ids[id];
-
-          if (Object.getOwnPropertyNames(ids).length == 0) {
-            delete this._data[docURI];
-          }
-        }
-
-        this.markAsChanged();
-      }
-    }
-  },
-
-  removeDocument(docURI) {
-    this.log("remove store values for doc=" + docURI);
-
-    if (!this._saveAllowed) {
-      Services.console.logStringMessage("XULStore: Changes after profile-before-change are ignored!");
-      return;
-    }
-
-    if (this._data[docURI]) {
-      delete this._data[docURI];
-      this.markAsChanged();
+      xulStore.setValue(uri, node.id, attr, value);
     }
   },
 
   getIDsEnumerator(docURI) {
-    this.log("Getting ID enumerator for doc=" + docURI);
-
-    if (!(docURI in this._data))
-      return new nsStringEnumerator([]);
-
-    let result = [];
-    let ids = this._data[docURI];
-    if (ids) {
-      for (let id in this._data[docURI]) {
-        result.push(id);
-      }
-    }
-
-    return new nsStringEnumerator(result);
+    return new XULStoreEnumerator(xulStore.getIDsEnumerator(docURI));
   },
 
   getAttributeEnumerator(docURI, id) {
-    this.log("Getting attribute enumerator for id=" + id + ", doc=" + docURI);
-
-    if (!(docURI in this._data) || !(id in this._data[docURI]))
-      return new nsStringEnumerator([]);
-
-    let attrs = [];
-    for (let attr in this._data[docURI][id]) {
-      attrs.push(attr);
-    }
-
-    return new nsStringEnumerator(attrs);
+    return new XULStoreEnumerator(xulStore.getAttributeEnumerator(docURI, id));
   },
 };
 
-function nsStringEnumerator(items) {
-  this._items = items;
-}
+class XULStoreEnumerator {
+  constructor(enumerator) {
+    this.enumerator = enumerator;
+  }
 
-nsStringEnumerator.prototype = {
-  QueryInterface: ChromeUtils.generateQI([Ci.nsIStringEnumerator]),
-  _nextIndex: 0,
-  [Symbol.iterator]() {
-    return this._items.values();
-  },
   hasMore() {
-    return this._nextIndex < this._items.length;
-  },
+    return this.enumerator.hasMore();
+  }
+
   getNext() {
-    if (!this.hasMore())
-      throw Cr.NS_ERROR_NOT_AVAILABLE;
-    return this._items[this._nextIndex++];
-  },
-};
+    return this.enumerator.getNext();
+  }
 
-var EXPORTED_SYMBOLS = ["XULStore"];
+  * [Symbol.iterator]() {
+    while (this.enumerator.hasMore()) {
+      yield (this.enumerator.getNext());
+    }
+  }
+}
--- a/toolkit/components/xulstore/components.conf
+++ b/toolkit/components/xulstore/components.conf
@@ -1,14 +1,16 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 Classes = [
     {
-        'cid': '{6f46b6f4-c8b1-4bd4-a4fa-9ebbed0753ea}',
+        'cid': '{be70bf11-0c28-4a02-a38c-0148538d42cf}',
         'contract_ids': ['@mozilla.org/xul/xulstore;1'],
-        'jsm': 'resource://gre/modules/XULStore.jsm',
-        'constructor': 'XULStore',
+        'type': 'nsIXULStore',
+        'headers': ['mozilla/XULStore.h'],
+        'singleton': True,
+        'constructor': 'mozilla::XULStore::GetService',
     },
 ]
--- a/toolkit/components/xulstore/moz.build
+++ b/toolkit/components/xulstore/moz.build
@@ -9,17 +9,31 @@ with Files('**'):
 
 MOCHITEST_CHROME_MANIFESTS += ['tests/chrome/chrome.ini']
 XPCSHELL_TESTS_MANIFESTS += ['tests/xpcshell/xpcshell.ini']
 
 XPIDL_SOURCES += [
     'nsIXULStore.idl',
 ]
 
-XPIDL_MODULE = 'toolkit_xulstore'
+TEST_DIRS += [
+    'tests/gtest',
+]
+
+EXPORTS.mozilla += [
+    'XULStore.h',
+]
 
 EXTRA_JS_MODULES += [
     'XULStore.jsm',
 ]
 
+XPIDL_MODULE = 'xulstore'
+
 XPCOM_MANIFESTS += [
     'components.conf',
 ]
+
+UNIFIED_SOURCES += [
+    'XULStore.cpp',
+]
+
+FINAL_LIBRARY = 'xul'
--- a/toolkit/components/xulstore/nsIXULStore.idl
+++ b/toolkit/components/xulstore/nsIXULStore.idl
@@ -1,40 +1,29 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "nsISupports.idl"
 
 interface nsIStringEnumerator;
-webidl Node;
 
 /**
  * The XUL store is used to store information related to a XUL document/application.
  * Typically it is used to store the persisted state for the document, such as
  * window location, toolbars that are open and nodes that are open and closed in a tree.
  *
- * The data is serialized to [profile directory]/xulstore.json
+ * XULStore.jsm wraps this API in useful abstractions for JS consumers.
+ * XULStore.h provides a more idiomatic API for C++ consumers.
+ * You should use those APIs unless you have good reasons to use this one.
  */
 [scriptable, uuid(987c4b35-c426-4dd7-ad49-3c9fa4c65d20)]
 interface nsIXULStore: nsISupports
 {
   /**
-   * Sets a value for a specified node's attribute, except in
-   * the case below (following the original XULDocument::persist):
-   * If the value is empty and if calling `hasValue` with the node's
-   * document and ID and `attr` would return true, then the
-   * value instead gets removed from the store (see Bug 1476680).
-   *
-   * @param node - DOM node
-   * @param attr - attribute to store
-   */
-  void persist(in Node aNode, in AString attr);
-
-  /**
    * Sets a value in the store.
    *
    * @param doc - document URI
    * @param id - identifier of the node
    * @param attr - attribute to store
    * @param value - value of the attribute
    */
   void setValue(in AString doc, in AString id, in AString attr, in AString value);
new file mode 100644
--- /dev/null
+++ b/toolkit/components/xulstore/src/error.rs
@@ -0,0 +1,112 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use nserror::{
+    nsresult, NS_ERROR_FAILURE, NS_ERROR_ILLEGAL_VALUE, NS_ERROR_NOT_AVAILABLE, NS_ERROR_UNEXPECTED,
+};
+use rkv::StoreError as RkvStoreError;
+use serde_json::Error as SerdeJsonError;
+use std::{io::Error as IoError, str::Utf8Error, string::FromUtf16Error, sync::PoisonError};
+
+pub(crate) type XULStoreResult<T> = Result<T, XULStoreError>;
+
+#[derive(Debug, Fail)]
+pub(crate) enum XULStoreError {
+    #[fail(display = "error converting bytes: {:?}", _0)]
+    ConvertBytes(Utf8Error),
+
+    #[fail(display = "error converting string: {:?}", _0)]
+    ConvertString(FromUtf16Error),
+
+    #[fail(display = "I/O error: {:?}", _0)]
+    IoError(IoError),
+
+    #[fail(display = "iteration is finished")]
+    IterationFinished,
+
+    #[fail(display = "JSON error: {}", _0)]
+    JsonError(SerdeJsonError),
+
+    #[fail(display = "error result {}", _0)]
+    NsResult(nsresult),
+
+    #[fail(display = "poison error getting read/write lock")]
+    PoisonError,
+
+    #[fail(display = "store error: {:?}", _0)]
+    RkvStoreError(RkvStoreError),
+
+    #[fail(display = "id or attribute name too long")]
+    IdAttrNameTooLong,
+
+    #[fail(display = "unavailable")]
+    Unavailable,
+
+    #[fail(display = "unexpected key: {:?}", _0)]
+    UnexpectedKey(String),
+
+    #[fail(display = "unexpected value")]
+    UnexpectedValue,
+}
+
+impl From<XULStoreError> for nsresult {
+    fn from(err: XULStoreError) -> nsresult {
+        match err {
+            XULStoreError::ConvertBytes(_) => NS_ERROR_FAILURE,
+            XULStoreError::ConvertString(_) => NS_ERROR_FAILURE,
+            XULStoreError::IoError(_) => NS_ERROR_FAILURE,
+            XULStoreError::IterationFinished => NS_ERROR_FAILURE,
+            XULStoreError::JsonError(_) => NS_ERROR_FAILURE,
+            XULStoreError::NsResult(result) => result,
+            XULStoreError::PoisonError => NS_ERROR_UNEXPECTED,
+            XULStoreError::RkvStoreError(_) => NS_ERROR_FAILURE,
+            XULStoreError::IdAttrNameTooLong => NS_ERROR_ILLEGAL_VALUE,
+            XULStoreError::Unavailable => NS_ERROR_NOT_AVAILABLE,
+            XULStoreError::UnexpectedKey(_) => NS_ERROR_UNEXPECTED,
+            XULStoreError::UnexpectedValue => NS_ERROR_UNEXPECTED,
+        }
+    }
+}
+
+impl From<FromUtf16Error> for XULStoreError {
+    fn from(err: FromUtf16Error) -> XULStoreError {
+        XULStoreError::ConvertString(err)
+    }
+}
+
+impl From<nsresult> for XULStoreError {
+    fn from(result: nsresult) -> XULStoreError {
+        XULStoreError::NsResult(result)
+    }
+}
+
+impl<T> From<PoisonError<T>> for XULStoreError {
+    fn from(_: PoisonError<T>) -> XULStoreError {
+        XULStoreError::PoisonError
+    }
+}
+
+impl From<RkvStoreError> for XULStoreError {
+    fn from(err: RkvStoreError) -> XULStoreError {
+        XULStoreError::RkvStoreError(err)
+    }
+}
+
+impl From<Utf8Error> for XULStoreError {
+    fn from(err: Utf8Error) -> XULStoreError {
+        XULStoreError::ConvertBytes(err)
+    }
+}
+
+impl From<IoError> for XULStoreError {
+    fn from(err: IoError) -> XULStoreError {
+        XULStoreError::IoError(err)
+    }
+}
+
+impl From<SerdeJsonError> for XULStoreError {
+    fn from(err: SerdeJsonError) -> XULStoreError {
+        XULStoreError::JsonError(err)
+    }
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/xulstore/src/ffi.rs
@@ -0,0 +1,339 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate as XULStore;
+use crate::{iter::XULStoreIterator, persist::clear_on_shutdown, statics::update_profile_dir};
+use libc::c_char;
+use nserror::{nsresult, NS_ERROR_NOT_IMPLEMENTED, NS_OK};
+use nsstring::{nsAString, nsString};
+use std::cell::RefCell;
+use std::ptr;
+use xpcom::{
+    interfaces::{nsIJSEnumerator, nsIStringEnumerator, nsISupports, nsIXULStore},
+    RefPtr,
+};
+
+#[no_mangle]
+pub unsafe extern "C" fn xulstore_new_service(result: *mut *const nsIXULStore) {
+    let xul_store_service = XULStoreService::new();
+    RefPtr::new(xul_store_service.coerce::<nsIXULStore>()).forget(&mut *result);
+}
+
+#[derive(xpcom)]
+#[xpimplements(nsIXULStore)]
+#[refcnt = "atomic"]
+pub struct InitXULStoreService {}
+
+impl XULStoreService {
+    fn new() -> RefPtr<XULStoreService> {
+        XULStoreService::allocate(InitXULStoreService {})
+    }
+
+    xpcom_method!(
+        set_value => SetValue(
+            doc: *const nsAString,
+            id: *const nsAString,
+            attr: *const nsAString,
+            value: *const nsAString
+        )
+    );
+
+    fn set_value(
+        &self,
+        doc: &nsAString,
+        id: &nsAString,
+        attr: &nsAString,
+        value: &nsAString,
+    ) -> Result<(), nsresult> {
+        XULStore::set_value(doc, id, attr, value).map_err(|err| err.into())
+    }
+
+    xpcom_method!(
+        has_value => HasValue(
+            doc: *const nsAString,
+            id: *const nsAString,
+            attr: *const nsAString
+        ) -> bool
+    );
+
+    fn has_value(
+        &self,
+        doc: &nsAString,
+        id: &nsAString,
+        attr: &nsAString,
+    ) -> Result<bool, nsresult> {
+        XULStore::has_value(doc, id, attr).map_err(|err| err.into())
+    }
+
+    xpcom_method!(
+        get_value => GetValue(
+            doc: *const nsAString,
+            id: *const nsAString,
+            attr: *const nsAString
+        ) -> nsAString
+    );
+
+    fn get_value(
+        &self,
+        doc: &nsAString,
+        id: &nsAString,
+        attr: &nsAString,
+    ) -> Result<nsString, nsresult> {
+        match XULStore::get_value(doc, id, attr) {
+            Ok(val) => Ok(nsString::from(&val)),
+            Err(err) => Err(err.into()),
+        }
+    }
+
+    xpcom_method!(
+        remove_value => RemoveValue(
+            doc: *const nsAString,
+            id: *const nsAString,
+            attr: *const nsAString
+        )
+    );
+
+    fn remove_value(
+        &self,
+        doc: &nsAString,
+        id: &nsAString,
+        attr: &nsAString,
+    ) -> Result<(), nsresult> {
+        XULStore::remove_value(doc, id, attr).map_err(|err| err.into())
+    }
+
+    xpcom_method!(
+        remove_document => RemoveDocument(doc: *const nsAString)
+    );
+
+    fn remove_document(&self, doc: &nsAString) -> Result<(), nsresult> {
+        XULStore::remove_document(doc).map_err(|err| err.into())
+    }
+
+    xpcom_method!(
+        get_ids_enumerator => GetIDsEnumerator(
+            doc: *const nsAString
+        ) -> * const nsIStringEnumerator
+    );
+
+    fn get_ids_enumerator(&self, doc: &nsAString) -> Result<RefPtr<nsIStringEnumerator>, nsresult> {
+        match XULStore::get_ids(doc) {
+            Ok(val) => {
+                let enumerator = StringEnumerator::new(val);
+                Ok(RefPtr::new(enumerator.coerce::<nsIStringEnumerator>()))
+            }
+            Err(err) => Err(err.into()),
+        }
+    }
+
+    xpcom_method!(
+        get_attribute_enumerator => GetAttributeEnumerator(
+            doc: *const nsAString,
+            id: *const nsAString
+        ) -> * const nsIStringEnumerator
+    );
+
+    fn get_attribute_enumerator(
+        &self,
+        doc: &nsAString,
+        id: &nsAString,
+    ) -> Result<RefPtr<nsIStringEnumerator>, nsresult> {
+        match XULStore::get_attrs(doc, id) {
+            Ok(val) => {
+                let enumerator = StringEnumerator::new(val);
+                Ok(RefPtr::new(enumerator.coerce::<nsIStringEnumerator>()))
+            }
+            Err(err) => Err(err.into()),
+        }
+    }
+}
+
+#[derive(xpcom)]
+#[xpimplements(nsIStringEnumerator)]
+#[refcnt = "nonatomic"]
+pub(crate) struct InitStringEnumerator {
+    iter: RefCell<XULStoreIterator>,
+}
+impl StringEnumerator {
+    pub(crate) fn new(iter: XULStoreIterator) -> RefPtr<StringEnumerator> {
+        StringEnumerator::allocate(InitStringEnumerator {
+            iter: RefCell::new(iter),
+        })
+    }
+
+    xpcom_method!(string_iterator => StringIterator() -> *const nsIJSEnumerator);
+
+    fn string_iterator(&self) -> Result<RefPtr<nsIJSEnumerator>, nsresult> {
+        Err(NS_ERROR_NOT_IMPLEMENTED)
+    }
+
+    xpcom_method!(has_more => HasMore() -> bool);
+
+    fn has_more(&self) -> Result<bool, nsresult> {
+        let iter = self.iter.borrow();
+        Ok(iter.has_more())
+    }
+
+    xpcom_method!(get_next => GetNext() -> nsAString);
+
+    fn get_next(&self) -> Result<nsString, nsresult> {
+        let mut iter = self.iter.borrow_mut();
+        match iter.get_next() {
+            Ok(value) => Ok(nsString::from(&value)),
+            Err(err) => Err(err.into()),
+        }
+    }
+}
+
+#[derive(xpcom)]
+#[xpimplements(nsIObserver)]
+#[refcnt = "nonatomic"]
+pub(crate) struct InitProfileChangeObserver {}
+impl ProfileChangeObserver {
+    #[allow(non_snake_case)]
+    unsafe fn Observe(
+        &self,
+        _subject: *const nsISupports,
+        _topic: *const c_char,
+        _data: *const i16,
+    ) -> nsresult {
+        update_profile_dir();
+        NS_OK
+    }
+
+    pub(crate) fn new() -> RefPtr<ProfileChangeObserver> {
+        ProfileChangeObserver::allocate(InitProfileChangeObserver {})
+    }
+}
+
+#[derive(xpcom)]
+#[xpimplements(nsIObserver)]
+#[refcnt = "nonatomic"]
+pub(crate) struct InitXpcomShutdownObserver {}
+impl XpcomShutdownObserver {
+    #[allow(non_snake_case)]
+    unsafe fn Observe(
+        &self,
+        _subject: *const nsISupports,
+        _topic: *const c_char,
+        _data: *const i16,
+    ) -> nsresult {
+        clear_on_shutdown();
+        NS_OK
+    }
+
+    pub(crate) fn new() -> RefPtr<XpcomShutdownObserver> {
+        XpcomShutdownObserver::allocate(InitXpcomShutdownObserver {})
+    }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn xulstore_set_value(
+    doc: &nsAString,
+    id: &nsAString,
+    attr: &nsAString,
+    value: &nsAString,
+) -> nsresult {
+    XULStore::set_value(doc, id, attr, value).into()
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn xulstore_has_value(
+    doc: &nsAString,
+    id: &nsAString,
+    attr: &nsAString,
+    has_value: *mut bool,
+) -> nsresult {
+    match XULStore::has_value(doc, id, attr) {
+        Ok(val) => {
+            *has_value = val;
+            NS_OK
+        }
+        Err(err) => err.into(),
+    }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn xulstore_get_value(
+    doc: &nsAString,
+    id: &nsAString,
+    attr: &nsAString,
+    value: *mut nsAString,
+) -> nsresult {
+    match XULStore::get_value(doc, id, attr) {
+        Ok(val) => {
+            (*value).assign(&nsString::from(&val));
+            NS_OK
+        }
+        Err(err) => err.into(),
+    }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn xulstore_remove_value(
+    doc: &nsAString,
+    id: &nsAString,
+    attr: &nsAString,
+) -> nsresult {
+    XULStore::remove_value(doc, id, attr).into()
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn xulstore_get_ids(
+    doc: &nsAString,
+    result: *mut nsresult,
+) -> *mut XULStoreIterator {
+    match XULStore::get_ids(doc) {
+        Ok(iter) => {
+            *result = NS_OK;
+            Box::into_raw(Box::new(iter))
+        }
+        Err(err) => {
+            *result = err.into();
+            ptr::null_mut()
+        }
+    }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn xulstore_get_attrs(
+    doc: &nsAString,
+    id: &nsAString,
+    result: *mut nsresult,
+) -> *mut XULStoreIterator {
+    match XULStore::get_attrs(doc, id) {
+        Ok(iter) => {
+            *result = NS_OK;
+            Box::into_raw(Box::new(iter))
+        }
+        Err(err) => {
+            *result = err.into();
+            ptr::null_mut()
+        }
+    }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn xulstore_iter_has_more(iter: &XULStoreIterator) -> bool {
+    iter.has_more()
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn xulstore_iter_get_next(
+    iter: &mut XULStoreIterator,
+    value: *mut nsAString,
+) -> nsresult {
+    match iter.get_next() {
+        Ok(val) => {
+            (*value).assign(&nsString::from(&val));
+            NS_OK
+        }
+        Err(err) => err.into(),
+    }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn xulstore_iter_free(iter: *mut XULStoreIterator) {
+    drop(Box::from_raw(iter));
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/xulstore/src/iter.rs
@@ -0,0 +1,24 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::error::{XULStoreError, XULStoreResult};
+use std::vec::IntoIter;
+
+pub struct XULStoreIterator {
+    values: IntoIter<String>,
+}
+
+impl XULStoreIterator {
+    pub(crate) fn new(values: IntoIter<String>) -> Self {
+        Self { values }
+    }
+
+    pub(crate) fn has_more(&self) -> bool {
+        !self.values.as_slice().is_empty()
+    }
+
+    pub(crate) fn get_next(&mut self) -> XULStoreResult<String> {
+        Ok(self.values.next().ok_or(XULStoreError::IterationFinished)?)
+    }
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/xulstore/src/lib.rs
@@ -0,0 +1,219 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+extern crate crossbeam_utils;
+#[macro_use]
+extern crate failure;
+#[macro_use]
+extern crate lazy_static;
+extern crate libc;
+extern crate lmdb;
+#[macro_use]
+extern crate log;
+extern crate moz_task;
+extern crate nserror;
+extern crate nsstring;
+extern crate rkv;
+extern crate serde_json;
+#[macro_use]
+extern crate xpcom;
+
+mod error;
+mod ffi;
+mod iter;
+mod persist;
+mod statics;
+
+use crate::{
+    error::{XULStoreError, XULStoreResult},
+    iter::XULStoreIterator,
+    persist::persist,
+    statics::DATA_CACHE,
+};
+use nsstring::nsAString;
+use std::collections::btree_map::Entry;
+use std::fmt::Display;
+
+const SEPARATOR: char = '\u{0009}';
+
+pub(crate) fn make_key(doc: &impl Display, id: &impl Display, attr: &impl Display) -> String {
+    format!("{}{}{}{}{}", doc, SEPARATOR, id, SEPARATOR, attr)
+}
+
+pub(crate) fn set_value(
+    doc: &nsAString,
+    id: &nsAString,
+    attr: &nsAString,
+    value: &nsAString,
+) -> XULStoreResult<()> {
+    debug!("XULStore set value: {} {} {} {}", doc, id, attr, value);
+
+    // bug 319846 -- don't save really long attributes or values.
+    if id.len() > 512 || attr.len() > 512 {
+        return Err(XULStoreError::IdAttrNameTooLong);
+    }
+
+    let value = if value.len() > 4096 {
+        warn!("XULStore: truncating long attribute value");
+        String::from_utf16(&value[0..4096])?
+    } else {
+        String::from_utf16(value)?
+    };
+
+    let mut cache_guard = DATA_CACHE.lock()?;
+    let data = match cache_guard.as_mut() {
+        Some(data) => data,
+        None => return Ok(()),
+    };
+    data.entry(doc.to_string())
+        .or_default()
+        .entry(id.to_string())
+        .or_default()
+        .insert(attr.to_string(), value.clone());
+
+    persist(make_key(doc, id, attr), Some(value))?;
+
+    Ok(())
+}
+
+pub(crate) fn has_value(doc: &nsAString, id: &nsAString, attr: &nsAString) -> XULStoreResult<bool> {
+    debug!("XULStore has value: {} {} {}", doc, id, attr);
+
+    let cache_guard = DATA_CACHE.lock()?;
+    let data = match cache_guard.as_ref() {
+        Some(data) => data,
+        None => return Ok(false),
+    };
+
+    match data.get(&doc.to_string()) {
+        Some(ids) => match ids.get(&id.to_string()) {
+            Some(attrs) => Ok(attrs.contains_key(&attr.to_string())),
+            None => Ok(false),
+        },
+        None => Ok(false),
+    }
+}
+
+pub(crate) fn get_value(
+    doc: &nsAString,
+    id: &nsAString,
+    attr: &nsAString,
+) -> XULStoreResult<String> {
+    debug!("XULStore get value {} {} {}", doc, id, attr);
+
+    let cache_guard = DATA_CACHE.lock()?;
+    let data = match cache_guard.as_ref() {
+        Some(data) => data,
+        None => return Ok(String::new()),
+    };
+
+    match data.get(&doc.to_string()) {
+        Some(ids) => match ids.get(&id.to_string()) {
+            Some(attrs) => match attrs.get(&attr.to_string()) {
+                Some(value) => Ok(value.clone()),
+                None => Ok(String::new()),
+            },
+            None => Ok(String::new()),
+        },
+        None => Ok(String::new()),
+    }
+}
+
+pub(crate) fn remove_value(
+    doc: &nsAString,
+    id: &nsAString,
+    attr: &nsAString,
+) -> XULStoreResult<()> {
+    debug!("XULStore remove value {} {} {}", doc, id, attr);
+
+    let mut cache_guard = DATA_CACHE.lock()?;
+    let data = match cache_guard.as_mut() {
+        Some(data) => data,
+        None => return Ok(()),
+    };
+
+    let mut ids_empty = false;
+    if let Some(ids) = data.get_mut(&doc.to_string()) {
+        let mut attrs_empty = false;
+        if let Some(attrs) = ids.get_mut(&id.to_string()) {
+            attrs.remove(&attr.to_string());
+            if attrs.is_empty() {
+                attrs_empty = true;
+            }
+        }
+        if attrs_empty {
+            ids.remove(&id.to_string());
+            if ids.is_empty() {
+                ids_empty = true;
+            }
+        }
+    };
+    if ids_empty {
+        data.remove(&doc.to_string());
+    }
+
+    persist(make_key(doc, id, attr), None)?;
+
+    Ok(())
+}
+
+pub(crate) fn remove_document(doc: &nsAString) -> XULStoreResult<()> {
+    debug!("XULStore remove document {}", doc);
+
+    let mut cache_guard = DATA_CACHE.lock()?;
+    let data = match cache_guard.as_mut() {
+        Some(data) => data,
+        None => return Ok(()),
+    };
+
+    if let Entry::Occupied(entry) = data.entry(doc.to_string()) {
+        for (id, attrs) in entry.get() {
+            for attr in attrs.keys() {
+                persist(make_key(entry.key(), id, attr), None)?;
+            }
+        }
+        entry.remove_entry();
+    }
+
+    Ok(())
+}
+
+pub(crate) fn get_ids(doc: &nsAString) -> XULStoreResult<XULStoreIterator> {
+    debug!("XULStore get IDs for {}", doc);
+
+    let cache_guard = DATA_CACHE.lock()?;
+    let data = match cache_guard.as_ref() {
+        Some(data) => data,
+        None => return Ok(XULStoreIterator::new(vec![].into_iter())),
+    };
+
+    match data.get(&doc.to_string()) {
+        Some(ids) => {
+            let mut ids: Vec<String> = ids.keys().cloned().collect();
+            Ok(XULStoreIterator::new(ids.into_iter()))
+        }
+        None => Ok(XULStoreIterator::new(vec![].into_iter())),
+    }
+}
+
+pub(crate) fn get_attrs(doc: &nsAString, id: &nsAString) -> XULStoreResult<XULStoreIterator> {
+    debug!("XULStore get attrs for doc, ID: {} {}", doc, id);
+
+    let cache_guard = DATA_CACHE.lock()?;
+    let data = match cache_guard.as_ref() {
+        Some(data) => data,
+        None => return Ok(XULStoreIterator::new(vec![].into_iter())),
+    };
+
+    match data.get(&doc.to_string()) {
+        Some(ids) => match ids.get(&id.to_string()) {
+            Some(attrs) => {
+                let mut attrs: Vec<String> = attrs.keys().cloned().collect();
+                Ok(XULStoreIterator::new(attrs.into_iter()))
+            }
+            None => Ok(XULStoreIterator::new(vec![].into_iter())),
+        },
+        None => Ok(XULStoreIterator::new(vec![].into_iter())),
+    }
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/xulstore/src/persist.rs
@@ -0,0 +1,188 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::{
+    error::{XULStoreError, XULStoreResult},
+    ffi::XpcomShutdownObserver,
+    statics::get_database,
+};
+use crossbeam_utils::atomic::AtomicCell;
+use lmdb::Error as LmdbError;
+use moz_task::{create_thread, Task, TaskRunnable};
+use nserror::nsresult;
+use rkv::{StoreError as RkvStoreError, Value};
+use std::{collections::HashMap, sync::Mutex, thread::sleep, time::Duration};
+use xpcom::{interfaces::nsIThread, RefPtr, ThreadBoundRefPtr};
+
+/// The XULStore API is synchronous for both C++ and JS consumers and accessed
+/// on the main thread, so we persist its data to disk on a background thread
+/// to avoid janking the UI.
+///
+/// We also re-open the database each time we write to it in order to conserve
+/// heap memory, since holding a database connection open would consume at least
+/// 3MB of heap memory in perpetuity.
+///
+/// Since re-opening the database repeatedly to write individual changes can be
+/// expensive when there are many of them in quick succession, we batch changes
+/// and write them in batches.
+
+lazy_static! {
+    /// A map of key/value pairs to persist.  Values are Options so we can
+    /// use the same structure for both puts and deletes, with a `None` value
+    /// identifying a key that should be deleted from the database.
+    ///
+    /// This is a map rather than a sequence in order to merge consecutive
+    /// changes to the same key, i.e. when a consumer sets *foo* to `bar`
+    /// and then sets it again to `baz` before we persist the first change.
+    ///
+    /// In that case, there's no point in setting *foo* to `bar` before we set
+    /// it to `baz`, and the map ensures we only ever persist the latest value
+    /// for any given key.
+    static ref CHANGES: Mutex<Option<HashMap<String, Option<String>>>> = { Mutex::new(None) };
+
+    /// A Mutex that prevents two PersistTasks from running at the same time,
+    /// since each task opens the database, and we need to ensure there is only
+    /// one open database handle for the database at any given time.
+    static ref PERSIST: Mutex<()> = { Mutex::new(()) };
+
+    static ref THREAD: Mutex<Option<ThreadBoundRefPtr<nsIThread>>> = {
+        let thread: RefPtr<nsIThread> = match create_thread("XULStore") {
+            Ok(thread) => thread,
+            Err(err) => {
+                error!("error creating XULStore thread: {}", err);
+                return Mutex::new(None);
+            }
+        };
+
+        // Observe XPCOM shutdown so we can clear the thread and thus not
+        // "leak" it (from the perspective of the leak checker).
+        observe_xpcom_shutdown();
+
+        Mutex::new(Some(ThreadBoundRefPtr::new(thread)))
+    };
+}
+
+fn observe_xpcom_shutdown() {
+    (|| -> XULStoreResult<()> {
+        let obs_svc = xpcom::services::get_ObserverService().ok_or(XULStoreError::Unavailable)?;
+        let observer = XpcomShutdownObserver::new();
+        unsafe {
+            obs_svc
+                .AddObserver(observer.coerce(), c_str!("xpcom-shutdown").as_ptr(), false)
+                .to_result()?
+        };
+        Ok(())
+    })()
+    .unwrap_or_else(|err| error!("error observing XPCOM shutdown: {}", err));
+}
+
+pub(crate) fn clear_on_shutdown() {
+    (|| -> XULStoreResult<()> {
+        THREAD.lock()?.take();
+        Ok(())
+    })()
+    .unwrap_or_else(|err| error!("error clearing thread: {}", err));
+}
+
+pub(crate) fn persist(key: String, value: Option<String>) -> XULStoreResult<()> {
+    let mut changes = CHANGES.lock()?;
+
+    if changes.is_none() {
+        *changes = Some(HashMap::new());
+
+        // If *changes* was `None`, then this is the first change since
+        // the last time we persisted, so dispatch a new PersistTask.
+        let task = Box::new(PersistTask::new());
+        let thread_guard = THREAD.lock()?;
+        let thread = thread_guard
+            .as_ref()
+            .ok_or(XULStoreError::Unavailable)?
+            .get_ref()
+            .ok_or(XULStoreError::Unavailable)?;
+        TaskRunnable::new("XULStore::Persist", task)?.dispatch(thread)?;
+    }
+
+    // Now insert the key/value pair into the map.  The unwrap() call here
+    // should never panic, since the code above sets `writes` to a Some(HashMap)
+    // if it's None.
+    changes.as_mut().unwrap().insert(key, value);
+
+    Ok(())
+}
+
+pub struct PersistTask {
+    result: AtomicCell<Option<Result<(), XULStoreError>>>,
+}
+
+impl PersistTask {
+    pub fn new() -> PersistTask {
+        PersistTask {
+            result: AtomicCell::default(),
+        }
+    }
+}
+
+impl Task for PersistTask {
+    fn run(&self) {
+        self.result.store(Some(|| -> Result<(), XULStoreError> {
+            // Avoid persisting too often.  We might want to adjust this value
+            // in the future to trade durability for performance.
+            sleep(Duration::from_millis(200));
+
+            // Prevent another PersistTask from running until this one finishes.
+            // We do this before getting the database to ensure that there is
+            // only ever one open database handle at a given time.
+            let _lock = PERSIST.lock()?;
+
+            let db = get_database()?;
+            let mut writer = db.env.write()?;
+
+            // Get the map of key/value pairs from the mutex, replacing it
+            // with None.  To avoid janking the main thread (if it decides
+            // to makes more changes while we're persisting to disk), we only
+            // lock the map long enough to move it out of the Mutex.
+            let writes = CHANGES.lock()?.take();
+
+            // The Option should be a Some(HashMap) (otherwise the task
+            // shouldn't have been scheduled in the first place).  If it's None,
+            // unexpectedly, then we return an error early.
+            let writes = writes.ok_or(XULStoreError::Unavailable)?;
+
+            for (key, value) in writes.iter() {
+                match value {
+                    Some(val) => db.store.put(&mut writer, &key, &Value::Str(val))?,
+                    None => {
+                        match db.store.delete(&mut writer, &key) {
+                            Ok(_) => (),
+
+                            // The XULStore API doesn't care if a consumer tries
+                            // to remove a value that doesn't exist in the store,
+                            // so we ignore the error (although in this case the key
+                            // should exist, since it was in the cache!).
+                            Err(RkvStoreError::LmdbError(LmdbError::NotFound)) => {
+                                warn!("tried to remove key that isn't in the store");
+                            }
+
+                            Err(err) => return Err(err.into()),
+                        }
+                    }
+                }
+            }
+
+            writer.commit()?;
+
+            Ok(())
+        }()));
+    }
+
+    fn done(&self) -> Result<(), nsresult> {
+        match self.result.swap(None) {
+            Some(Ok(())) => (),
+            Some(Err(err)) => error!("removeDocument error: {}", err),
+            None => error!("removeDocument error: unexpected result"),
+        };
+
+        Ok(())
+    }
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/xulstore/src/statics.rs
@@ -0,0 +1,249 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::{
+    error::{XULStoreError, XULStoreResult},
+    ffi::ProfileChangeObserver,
+    make_key, SEPARATOR,
+};
+use moz_task::is_main_thread;
+use nsstring::nsString;
+use rkv::{Rkv, SingleStore, StoreOptions, Value};
+use std::{
+    collections::BTreeMap,
+    fs::{create_dir_all, remove_file, File},
+    path::PathBuf,
+    str,
+    sync::Mutex,
+};
+use xpcom::{interfaces::nsIFile, XpCom};
+
+type XULStoreCache = BTreeMap<String, BTreeMap<String, BTreeMap<String, String>>>;
+
+pub struct Database {
+    pub env: Rkv,
+    pub store: SingleStore,
+}
+
+impl Database {
+    fn new(env: Rkv, store: SingleStore) -> Database {
+        Database { env, store }
+    }
+}
+
+lazy_static! {
+    static ref PROFILE_DIR: Mutex<Option<PathBuf>> = {
+        observe_profile_change();
+        Mutex::new(get_profile_dir().ok())
+    };
+    pub(crate) static ref DATA_CACHE: Mutex<Option<XULStoreCache>> =
+        { Mutex::new(cache_data().ok()) };
+}
+
+pub(crate) fn get_database() -> XULStoreResult<Database> {
+    let xulstore_dir = get_xulstore_dir()?;
+    let env = Rkv::new(xulstore_dir.as_path())?;
+    let store = env.open_single("db", StoreOptions::create())?;
+
+    Ok(Database::new(env, store))
+}
+
+pub(crate) fn update_profile_dir() {
+    // Failure to update the dir isn't fatal (although it means that we won't
+    // persist XULStore data for this session), so we don't return a result.
+    // But we use a closure returning a result to enable use of the ? operator.
+    (|| -> XULStoreResult<()> {
+        {
+            let mut profile_dir_guard = PROFILE_DIR.lock()?;
+            *profile_dir_guard = get_profile_dir().ok();
+        }
+
+        let mut cache_guard = DATA_CACHE.lock()?;
+        *cache_guard = cache_data().ok();
+
+        Ok(())
+    })()
+    .unwrap_or_else(|err| error!("error updating profile dir: {}", err));
+}
+
+fn get_profile_dir() -> XULStoreResult<PathBuf> {
+    // We can't use getter_addrefs() here because get_DirectoryService()
+    // returns its nsIProperties interface, and its Get() method returns
+    // a directory via its nsQIResult out param, which gets translated to
+    // a `*mut *mut libc::c_void` in Rust, whereas getter_addrefs() expects
+    // a closure with a `*mut *const T` parameter.
+
+    let dir_svc = xpcom::services::get_DirectoryService().ok_or(XULStoreError::Unavailable)?;
+    let mut profile_dir = xpcom::GetterAddrefs::<nsIFile>::new();
+    unsafe {
+        dir_svc
+            .Get(
+                c_str!("ProfD").as_ptr(),
+                &nsIFile::IID,
+                profile_dir.void_ptr(),
+            )
+            .to_result()
+            .or_else(|_| {
+                dir_svc
+                    .Get(
+                        c_str!("ProfDS").as_ptr(),
+                        &nsIFile::IID,
+                        profile_dir.void_ptr(),
+                    )
+                    .to_result()
+            })?;
+    }
+    let profile_dir = profile_dir.refptr().ok_or(XULStoreError::Unavailable)?;
+
+    let mut profile_path = nsString::new();
+    unsafe {
+        profile_dir.GetPath(&mut *profile_path).to_result()?;
+    }
+
+    let path = String::from_utf16(&profile_path[..])?;
+    Ok(PathBuf::from(&path))
+}
+
+fn get_xulstore_dir() -> XULStoreResult<PathBuf> {
+    let mut xulstore_dir = PROFILE_DIR
+        .lock()?
+        .clone()
+        .ok_or(XULStoreError::Unavailable)?;
+    xulstore_dir.push("xulstore");
+
+    create_dir_all(xulstore_dir.clone())?;
+
+    Ok(xulstore_dir)
+}
+
+fn observe_profile_change() {
+    assert!(is_main_thread());
+
+    // Failure to observe the change isn't fatal (although it means we won't
+    // persist XULStore data for this session), so we don't return a result.
+    // But we use a closure returning a result to enable use of the ? operator.
+    (|| -> XULStoreResult<()> {
+        // Observe profile changes so we can update this directory accordingly.
+        let obs_svc = xpcom::services::get_ObserverService().ok_or(XULStoreError::Unavailable)?;
+        let observer = ProfileChangeObserver::new();
+        unsafe {
+            obs_svc
+                .AddObserver(
+                    observer.coerce(),
+                    c_str!("profile-after-change").as_ptr(),
+                    false,
+                )
+                .to_result()?
+        };
+        Ok(())
+    })()
+    .unwrap_or_else(|err| error!("error observing profile change: {}", err));
+}
+
+fn in_safe_mode() -> XULStoreResult<bool> {
+    let app_info_svc = xpcom::services::get_AppInfoService().ok_or(XULStoreError::Unavailable)?;
+    let mut in_safe_mode = false;
+    unsafe {
+        app_info_svc.GetInSafeMode(&mut in_safe_mode).to_result()?;
+    }
+    Ok(in_safe_mode)
+}
+
+fn cache_data() -> XULStoreResult<XULStoreCache> {
+    let db = get_database()?;
+    maybe_migrate_data(&db.env, db.store);
+
+    let mut all = XULStoreCache::default();
+    if in_safe_mode()? {
+        return Ok(all);
+    }
+
+    let reader = db.env.read()?;
+    let iterator = db.store.iter_start(&reader)?;
+
+    for result in iterator {
+        let (key, value): (&str, String) = match result {
+            Ok((key, value)) => {
+                assert!(value.is_some(), "iterated key has value");
+                match (str::from_utf8(&key), unwrap_value(&value)) {
+                    (Ok(key), Ok(value)) => (key, value),
+                    (Err(err), _) => return Err(err.into()),
+                    (_, Err(err)) => return Err(err),
+                }
+            }
+            Err(err) => return Err(err.into()),
+        };
+
+        let parts = key.split(SEPARATOR).collect::<Vec<&str>>();
+        if parts.len() != 3 {
+            return Err(XULStoreError::UnexpectedKey(key.to_string()));
+        }
+        let (doc, id, attr) = (
+            parts[0].to_string(),
+            parts[1].to_string(),
+            parts[2].to_string(),
+        );
+
+        all.entry(doc)
+            .or_default()
+            .entry(id)
+            .or_default()
+            .entry(attr)
+            .or_insert(value);
+    }
+
+    Ok(all)
+}
+
+fn maybe_migrate_data(env: &Rkv, store: SingleStore) {
+    // Failure to migrate data isn't fatal, so we don't return a result.
+    // But we use a closure returning a result to enable use of the ? operator.
+    (|| -> XULStoreResult<()> {
+        let mut old_datastore = PROFILE_DIR
+            .lock()?
+            .clone()
+            .ok_or(XULStoreError::Unavailable)?;
+        old_datastore.push("xulstore.json");
+        if !old_datastore.exists() {
+            debug!("old datastore doesn't exist: {:?}", old_datastore);
+            return Ok(());
+        }
+
+        let file = File::open(old_datastore.clone())?;
+        let json: XULStoreCache = serde_json::from_reader(file)?;
+
+        let mut writer = env.write()?;
+
+        for (doc, ids) in json {
+            for (id, attrs) in ids {
+                for (attr, value) in attrs {
+                    let key = make_key(&doc, &id, &attr);
+                    store.put(&mut writer, &key, &Value::Str(&value))?;
+                }
+            }
+        }
+
+        writer.commit()?;
+
+        remove_file(old_datastore)?;
+
+        Ok(())
+    })()
+    .unwrap_or_else(|err| error!("error migrating data: {}", err));
+}
+
+fn unwrap_value(value: &Option<Value>) -> XULStoreResult<String> {
+    match value {
+        Some(Value::Str(val)) => Ok(val.to_string()),
+
+        // Per the XULStore API, return an empty string if the value
+        // isn't found.
+        None => Ok(String::new()),
+
+        // This should never happen, but it could happen in theory
+        // if someone writes a different kind of value into the store
+        // using a more general API (kvstore, rkv, LMDB).
+        Some(_) => Err(XULStoreError::UnexpectedValue),
+    }
+}
--- a/toolkit/components/xulstore/tests/chrome/window_persistence.xul
+++ b/toolkit/components/xulstore/tests/chrome/window_persistence.xul
@@ -8,17 +8,17 @@
         persist="screenX screenY width height">
 
 <button id="button1" label="Button1" persist="value"/>
 <button id="button2" label="Button2" value="Normal" persist="value"/>
 
 <script>
 <![CDATA[
 
-let XULStore = Cc["@mozilla.org/xul/xulstore;1"].getService(Ci.nsIXULStore);
+const {XULStore} = ChromeUtils.import("resource://gre/modules/XULStore.jsm");
 let URI = "chrome://mochitests/content/chrome/toolkit/components/xulstore/tests/chrome/window_persistence.xul";
 
 function opened()
 {
   runTest();
 }
 
 function runTest()
new file mode 100644
--- /dev/null
+++ b/toolkit/components/xulstore/tests/gtest/Cargo.toml
@@ -0,0 +1,7 @@
+[package]
+name = "xulstore-gtest"
+version = "0.1.0"
+authors = ["nobody@mozilla.org"]
+
+[lib]
+path = "test.rs"
new file mode 100644
--- /dev/null
+++ b/toolkit/components/xulstore/tests/gtest/TestXULStore.cpp
@@ -0,0 +1,141 @@
+#include <stdint.h>
+#include "gtest/gtest.h"
+#include "mozilla/XULStore.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+
+using mozilla::XULStoreIterator;
+using mozilla::XULStore::GetAttrs;
+using mozilla::XULStore::GetIDs;
+using mozilla::XULStore::GetValue;
+using mozilla::XULStore::HasValue;
+using mozilla::XULStore::RemoveValue;
+using mozilla::XULStore::SetValue;
+
+TEST(XULStore, SetGetValue)
+{
+  nsAutoString doc(NS_LITERAL_STRING("SetGetValue"));
+  nsAutoString id(NS_LITERAL_STRING("foo"));
+  nsAutoString attr(NS_LITERAL_STRING("bar"));
+  nsAutoString value;
+
+  EXPECT_EQ(GetValue(doc, id, attr, value), NS_OK);
+  EXPECT_TRUE(value.EqualsASCII(""));
+
+  {
+    nsAutoString value(NS_LITERAL_STRING("baz"));
+    EXPECT_EQ(SetValue(doc, id, attr, value), NS_OK);
+  }
+
+  EXPECT_EQ(GetValue(doc, id, attr, value), NS_OK);
+  EXPECT_TRUE(value.EqualsASCII("baz"));
+}
+
+TEST(XULStore, HasValue)
+{
+  nsAutoString doc(NS_LITERAL_STRING("HasValue"));
+  nsAutoString id(NS_LITERAL_STRING("foo"));
+  nsAutoString attr(NS_LITERAL_STRING("bar"));
+  bool hasValue = true;
+  EXPECT_EQ(HasValue(doc, id, attr, hasValue), NS_OK);
+  EXPECT_FALSE(hasValue);
+  nsAutoString value(NS_LITERAL_STRING("baz"));
+  EXPECT_EQ(SetValue(doc, id, attr, value), NS_OK);
+  EXPECT_EQ(HasValue(doc, id, attr, hasValue), NS_OK);
+  EXPECT_TRUE(hasValue);
+}
+
+TEST(XULStore, RemoveValue)
+{
+  nsAutoString doc(NS_LITERAL_STRING("RemoveValue"));
+  nsAutoString id(NS_LITERAL_STRING("foo"));
+  nsAutoString attr(NS_LITERAL_STRING("bar"));
+  nsAutoString value(NS_LITERAL_STRING("baz"));
+  EXPECT_EQ(SetValue(doc, id, attr, value), NS_OK);
+  EXPECT_EQ(GetValue(doc, id, attr, value), NS_OK);
+  EXPECT_TRUE(value.EqualsASCII("baz"));
+  EXPECT_EQ(RemoveValue(doc, id, attr), NS_OK);
+  EXPECT_EQ(GetValue(doc, id, attr, value), NS_OK);
+  EXPECT_TRUE(value.EqualsASCII(""));
+}
+
+TEST(XULStore, GetIDsIterator)
+{
+  nsAutoString doc(NS_LITERAL_STRING("idIterDoc"));
+  nsAutoString id1(NS_LITERAL_STRING("id1"));
+  nsAutoString id2(NS_LITERAL_STRING("id2"));
+  nsAutoString id3(NS_LITERAL_STRING("id3"));
+  nsAutoString attr(NS_LITERAL_STRING("attr"));
+  nsAutoString value(NS_LITERAL_STRING("value"));
+  nsAutoString id;
+
+  // Confirm that the store doesn't have any IDs yet.
+  mozilla::UniquePtr<XULStoreIterator> iter;
+  EXPECT_EQ(GetIDs(doc, iter), NS_OK);
+  EXPECT_FALSE(iter->HasMore());
+  // EXPECT_EQ(iter->GetNext(&id), NS_ERROR_FAILURE);
+
+  // Insert with IDs in non-alphanumeric order to confirm
+  // that store will order them when iterating them.
+  EXPECT_EQ(SetValue(doc, id3, attr, value), NS_OK);
+  EXPECT_EQ(SetValue(doc, id1, attr, value), NS_OK);
+  EXPECT_EQ(SetValue(doc, id2, attr, value), NS_OK);
+
+  // Insert different ID for another doc to confirm that store
+  // won't return it when iterating IDs for our doc.
+  nsAutoString otherDoc(NS_LITERAL_STRING("otherDoc"));
+  nsAutoString otherID(NS_LITERAL_STRING("otherID"));
+  EXPECT_EQ(SetValue(otherDoc, otherID, attr, value), NS_OK);
+
+  EXPECT_EQ(GetIDs(doc, iter), NS_OK);
+  EXPECT_TRUE(iter->HasMore());
+  EXPECT_EQ(iter->GetNext(&id), NS_OK);
+  EXPECT_TRUE(id.EqualsASCII("id1"));
+  EXPECT_TRUE(iter->HasMore());
+  EXPECT_EQ(iter->GetNext(&id), NS_OK);
+  EXPECT_TRUE(id.EqualsASCII("id2"));
+  EXPECT_TRUE(iter->HasMore());
+  EXPECT_EQ(iter->GetNext(&id), NS_OK);
+  EXPECT_TRUE(id.EqualsASCII("id3"));
+  EXPECT_FALSE(iter->HasMore());
+}
+
+TEST(XULStore, GetAttributeIterator)
+{
+  nsAutoString doc(NS_LITERAL_STRING("attrIterDoc"));
+  nsAutoString id(NS_LITERAL_STRING("id"));
+  nsAutoString attr1(NS_LITERAL_STRING("attr1"));
+  nsAutoString attr2(NS_LITERAL_STRING("attr2"));
+  nsAutoString attr3(NS_LITERAL_STRING("attr3"));
+  nsAutoString value(NS_LITERAL_STRING("value"));
+  nsAutoString attr;
+
+  mozilla::UniquePtr<XULStoreIterator> iter;
+  EXPECT_EQ(GetAttrs(doc, id, iter), NS_OK);
+  EXPECT_FALSE(iter->HasMore());
+  // EXPECT_EQ(iter->GetNext(&attr), NS_ERROR_FAILURE);
+
+  // Insert with attributes in non-alphanumeric order to confirm
+  // that store will order them when iterating them.
+  EXPECT_EQ(SetValue(doc, id, attr3, value), NS_OK);
+  EXPECT_EQ(SetValue(doc, id, attr1, value), NS_OK);
+  EXPECT_EQ(SetValue(doc, id, attr2, value), NS_OK);
+
+  // Insert different attribute for another ID to confirm that store
+  // won't return it when iterating attributes for our ID.
+  nsAutoString otherID(NS_LITERAL_STRING("otherID"));
+  nsAutoString otherAttr(NS_LITERAL_STRING("otherAttr"));
+  EXPECT_EQ(SetValue(doc, otherID, otherAttr, value), NS_OK);
+
+  EXPECT_EQ(GetAttrs(doc, id, iter), NS_OK);
+  EXPECT_TRUE(iter->HasMore());
+  EXPECT_EQ(iter->GetNext(&attr), NS_OK);
+  EXPECT_TRUE(attr.EqualsASCII("attr1"));
+  EXPECT_TRUE(iter->HasMore());
+  EXPECT_EQ(iter->GetNext(&attr), NS_OK);
+  EXPECT_TRUE(attr.EqualsASCII("attr2"));
+  EXPECT_TRUE(iter->HasMore());
+  EXPECT_EQ(iter->GetNext(&attr), NS_OK);
+  EXPECT_TRUE(attr.EqualsASCII("attr3"));
+  EXPECT_FALSE(iter->HasMore());
+}
copy from toolkit/components/xulstore/components.conf
copy to toolkit/components/xulstore/tests/gtest/moz.build
--- a/toolkit/components/xulstore/components.conf
+++ b/toolkit/components/xulstore/tests/gtest/moz.build
@@ -1,14 +1,14 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
-Classes = [
-    {
-        'cid': '{6f46b6f4-c8b1-4bd4-a4fa-9ebbed0753ea}',
-        'contract_ids': ['@mozilla.org/xul/xulstore;1'],
-        'jsm': 'resource://gre/modules/XULStore.jsm',
-        'constructor': 'XULStore',
-    },
+UNIFIED_SOURCES += [
+    'TestXULStore.cpp',
 ]
+
+FINAL_LIBRARY = 'xul-gtest'
+
+if CONFIG['CC_TYPE'] in ('clang', 'gcc'):
+    CXXFLAGS += ['-Wno-error=shadow']
new file mode 100644
--- /dev/null
+++ b/toolkit/components/xulstore/tests/xpcshell/test_XULStore_migration.js
@@ -0,0 +1,71 @@
+"use strict";
+
+const {OS} = ChromeUtils.import("resource://gre/modules/osfile.jsm");
+
+function run_test() {
+  do_get_profile();
+  run_next_test();
+}
+
+add_task(async function test_create_old_datastore() {
+  const path = OS.Path.join(OS.Constants.Path.profileDir, "xulstore.json");
+
+  const xulstoreJSON = {
+    doc1: {
+      id1: {
+        attr1: "value1",
+      },
+    },
+    doc2: {
+      id1: {
+        attr2: "value2",
+      },
+      id2: {
+        attr1: "value1",
+        attr2: "value2",
+        attr3: "value3",
+      },
+      id3: {},
+    },
+    doc3: {},
+  };
+
+  await OS.File.writeAtomic(path, JSON.stringify(xulstoreJSON));
+});
+
+add_task(async function test_get_values() {
+  // We wait until now to import XULStore.jsm to ensure we've created
+  // the old datastore, as importing that module will initiate the attempt
+  // to migrate the old datastore to the new one.
+  const {XULStore} = ChromeUtils.import("resource://gre/modules/XULStore.jsm");
+
+  Assert.equal(await XULStore.getValue("doc1", "id1", "attr1"), "value1");
+  Assert.equal(await XULStore.getValue("doc1", "id1", "attr2"), "");
+  Assert.equal(await XULStore.getValue("doc1", "id1", "attr3"), "");
+  Assert.equal(await XULStore.getValue("doc1", "id2", "attr1"), "");
+  Assert.equal(await XULStore.getValue("doc1", "id2", "attr2"), "");
+  Assert.equal(await XULStore.getValue("doc1", "id2", "attr3"), "");
+  Assert.equal(await XULStore.getValue("doc1", "id3", "attr1"), "");
+  Assert.equal(await XULStore.getValue("doc1", "id3", "attr2"), "");
+  Assert.equal(await XULStore.getValue("doc1", "id3", "attr3"), "");
+
+  Assert.equal(await XULStore.getValue("doc2", "id1", "attr1"), "");
+  Assert.equal(await XULStore.getValue("doc2", "id1", "attr2"), "value2");
+  Assert.equal(await XULStore.getValue("doc2", "id1", "attr3"), "");
+  Assert.equal(await XULStore.getValue("doc2", "id2", "attr1"), "value1");
+  Assert.equal(await XULStore.getValue("doc2", "id2", "attr2"), "value2");
+  Assert.equal(await XULStore.getValue("doc2", "id2", "attr3"), "value3");
+  Assert.equal(await XULStore.getValue("doc2", "id3", "attr1"), "");
+  Assert.equal(await XULStore.getValue("doc2", "id3", "attr2"), "");
+  Assert.equal(await XULStore.getValue("doc2", "id3", "attr3"), "");
+
+  Assert.equal(await XULStore.getValue("doc3", "id1", "attr1"), "");
+  Assert.equal(await XULStore.getValue("doc3", "id1", "attr2"), "");
+  Assert.equal(await XULStore.getValue("doc3", "id1", "attr3"), "");
+  Assert.equal(await XULStore.getValue("doc3", "id2", "attr1"), "");
+  Assert.equal(await XULStore.getValue("doc3", "id2", "attr2"), "");
+  Assert.equal(await XULStore.getValue("doc3", "id2", "attr3"), "");
+  Assert.equal(await XULStore.getValue("doc3", "id3", "attr1"), "");
+  Assert.equal(await XULStore.getValue("doc3", "id3", "attr2"), "");
+  Assert.equal(await XULStore.getValue("doc3", "id3", "attr3"), "");
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/xulstore/tests/xpcshell/test_XULStore_migration_fail_invalid_data.js
@@ -0,0 +1,43 @@
+"use strict";
+
+const {OS} = ChromeUtils.import("resource://gre/modules/osfile.jsm");
+
+function run_test() {
+  do_get_profile();
+  run_next_test();
+}
+
+add_task(async function test_create_old_datastore() {
+  const path = OS.Path.join(OS.Constants.Path.profileDir, "xulstore.json");
+
+  // Valid JSON, but invalid data: attr1's value is a number, not a string.
+  const xulstoreJSON = {
+    doc1: {
+      id1: {
+        attr1: 1,
+      },
+    },
+    doc2: {
+      id2: {
+        attr2: "value2",
+      },
+    },
+  };
+
+  await OS.File.writeAtomic(path, JSON.stringify(xulstoreJSON));
+});
+
+add_task(async function test_get_values() {
+  // We wait until now to import XULStore.jsm to ensure we've created
+  // the old store, as importing that module will initiate the attempt
+  // to migrate the old store to the new one.
+  const {XULStore} = ChromeUtils.import("resource://gre/modules/XULStore.jsm");
+
+  // XULStore should *not* have migrated the values from the old store,
+  // so it should return empty strings when we try to retrieve them.
+  // That's true for both values, even though one of them is valid,
+  // because the migrator uses a typed parser that requires the entire
+  // JSON file to conform to the XULStore format.
+  Assert.equal(await XULStore.getValue("doc1", "id1", "attr1"), "");
+  Assert.equal(await XULStore.getValue("doc2", "id2", "attr2"), "");
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/xulstore/tests/xpcshell/test_XULStore_migration_fail_invalid_json.js
@@ -0,0 +1,28 @@
+"use strict";
+
+const {OS} = ChromeUtils.import("resource://gre/modules/osfile.jsm");
+
+function run_test() {
+  do_get_profile();
+  run_next_test();
+}
+
+add_task(async function test_create_old_datastore() {
+  const path = OS.Path.join(OS.Constants.Path.profileDir, "xulstore.json");
+
+  // Invalid JSON: it's missing the final closing brace.
+  const xulstoreJSON = '{ doc: { id: { attr: "value" } }';
+
+  await OS.File.writeAtomic(path, xulstoreJSON);
+});
+
+add_task(async function test_get_value() {
+  // We wait until now to import XULStore.jsm to ensure we've created
+  // the old store, as importing that module will initiate the attempt
+  // to migrate the old store to the new one.
+  const {XULStore} = ChromeUtils.import("resource://gre/modules/XULStore.jsm");
+
+  // XULStore should *not* have migrated the value from the old store,
+  // so it should return an empty string when we try to retrieve it.
+  Assert.equal(await XULStore.getValue("doc", "id", "attr"), "");
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/xulstore/tests/xpcshell/test_XULStore_migration_profile_change.js
@@ -0,0 +1,43 @@
+"use strict";
+
+const {OS} = ChromeUtils.import("resource://gre/modules/osfile.jsm");
+const {FileUtils} = ChromeUtils.import("resource://gre/modules/FileUtils.jsm");
+const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+add_task(async function test_get_values() {
+  // Import XULStore.jsm before getting the profile to ensure that the new store
+  // is initialized, as the purpose of this test is to confirm that the old
+  // store data gets migrated if the profile change happens post-initialization.
+  const {XULStore} = ChromeUtils.import("resource://gre/modules/XULStore.jsm");
+
+  // We haven't migrated any data yet (nor even changed to a profile), so there
+  // shouldn't be a value in the store.
+  Assert.equal(XULStore.getValue("doc1", "id1", "attr1"), "");
+
+  // Register an observer before the XULStore service registers its observer,
+  // so we can observe the profile-after-change notification first and create
+  // an old store for it to migrate.  We need to write synchronously to avoid
+  // racing XULStore, so we use FileUtils instead of OS.File.
+  Services.obs.addObserver({
+    observe() {
+      const file = FileUtils.getFile("ProfD", ["xulstore.json"]);
+      const xulstoreJSON = JSON.stringify({
+        doc1: {
+          id1: {
+            attr1: "value1",
+          },
+        },
+      });
+      let stream = FileUtils.openAtomicFileOutputStream(file);
+      stream.write(xulstoreJSON, xulstoreJSON.length);
+      FileUtils.closeAtomicFileOutputStream(stream);
+    },
+  }, "profile-after-change");
+
+  // This creates a profile and changes to it, triggering first our
+  // profile-after-change observer above and then XULStore's equivalent.
+  do_get_profile(true);
+
+  // XULStore should now have migrated the value from the old store.
+  Assert.equal(XULStore.getValue("doc1", "id1", "attr1"), "value1");
+});
--- a/toolkit/components/xulstore/tests/xpcshell/xpcshell.ini
+++ b/toolkit/components/xulstore/tests/xpcshell/xpcshell.ini
@@ -1,4 +1,8 @@
 [DEFAULT]
 skip-if = toolkit == 'android'
 
 [test_XULStore.js]
+[test_XULStore_migration.js]
+[test_XULStore_migration_fail_invalid_json.js]
+[test_XULStore_migration_fail_invalid_data.js]
+[test_XULStore_migration_profile_change.js]
--- a/toolkit/library/rust/shared/Cargo.toml
+++ b/toolkit/library/rust/shared/Cargo.toml
@@ -22,16 +22,17 @@ cubeb-pulse = { path = "../../../../medi
 cubeb-sys = { version = "0.5.0", optional = true, features=["gecko-in-tree"] }
 encoding_c = "0.9.0"
 encoding_glue = { path = "../../../../intl/encoding_glue" }
 audioipc-client = { path = "../../../../media/audioipc/client", optional = true }
 audioipc-server = { path = "../../../../media/audioipc/server", optional = true }
 u2fhid = { path = "../../../../dom/webauthn/u2f-hid-rs" }
 gkrust_utils = { path = "../../../../xpcom/rust/gkrust_utils" }
 rsdparsa_capi = { path = "../../../../media/webrtc/signaling/src/sdp/rsdparsa_capi" }
+xulstore = { path = "../../../components/xulstore" }
 # We have these to enforce common feature sets for said crates.
 log = {version = "0.4", features = ["release_max_level_info"]}
 env_logger = {version = "0.5", default-features = false} # disable `regex` to reduce code size
 cose-c = { version = "0.1.5" }
 jsrust_shared = { path = "../../../../js/src/rust/shared", optional = true }
 arrayvec = "0.4"
 cert_storage = { path = "../../../../security/manager/ssl/cert_storage" }
 bitsdownload = { path = "../../../components/bitsdownload", optional = true }
--- a/toolkit/library/rust/shared/lib.rs
+++ b/toolkit/library/rust/shared/lib.rs
@@ -29,16 +29,17 @@ extern crate audioipc_client;
 extern crate audioipc_server;
 extern crate env_logger;
 extern crate u2fhid;
 extern crate gkrust_utils;
 extern crate log;
 extern crate cert_storage;
 extern crate cosec;
 extern crate rsdparsa_capi;
+extern crate xulstore;
 #[cfg(feature = "spidermonkey_rust")]
 extern crate jsrust_shared;
 #[cfg(feature = "bitsdownload")]
 extern crate bitsdownload;
 extern crate storage;
 #[cfg(feature = "moz_places")]
 extern crate bookmark_sync;
 
--- a/toolkit/modules/Services.jsm
+++ b/toolkit/modules/Services.jsm
@@ -46,16 +46,21 @@ if (AppConstants.MOZ_CRASHREPORTER) {
   XPCOMUtils.defineLazyGetter(Services, "crashmanager", () => {
     let ns = {};
     ChromeUtils.import("resource://gre/modules/CrashManager.jsm", ns);
 
     return ns.CrashManager.Singleton;
   });
 }
 
+XPCOMUtils.defineLazyGetter(Services, "xulStore", () => {
+  const {XULStore} = ChromeUtils.import("resource://gre/modules/XULStore.jsm");
+  return XULStore;
+});
+
 XPCOMUtils.defineLazyGetter(Services, "io", () => {
   return Cc["@mozilla.org/network/io-service;1"]
            .getService(Ci.nsIIOService)
            .QueryInterface(Ci.nsISpeculativeConnect);
 });
 
 var initTable = {
   appShell: ["@mozilla.org/appshell/appShellService;1", "nsIAppShellService"],
@@ -94,17 +99,16 @@ var initTable = {
   clipboard: ["@mozilla.org/widget/clipboard;1", "nsIClipboard"],
   DOMRequest: ["@mozilla.org/dom/dom-request-service;1", "nsIDOMRequestService"],
   focus: ["@mozilla.org/focus-manager;1", "nsIFocusManager"],
   uriFixup: ["@mozilla.org/docshell/urifixup;1", "nsIURIFixup"],
   blocklist: ["@mozilla.org/extensions/blocklist;1"],
   netUtils: ["@mozilla.org/network/util;1", "nsINetUtil"],
   loadContextInfo: ["@mozilla.org/load-context-info-factory;1", "nsILoadContextInfoFactory"],
   qms: ["@mozilla.org/dom/quota-manager-service;1", "nsIQuotaManagerService"],
-  xulStore: ["@mozilla.org/xul/xulstore;1", "nsIXULStore"],
 };
 
 if (AppConstants.platform == "android") {
   initTable.androidBridge = ["@mozilla.org/android/bridge;1", "nsIAndroidBridge"];
 }
 if (AppConstants.MOZ_TOOLKIT_SEARCH) {
   initTable.search = ["@mozilla.org/browser/search-service;1", "nsISearchService"];
 }
--- a/xpcom/build/Services.py
+++ b/xpcom/build/Services.py
@@ -47,16 +47,19 @@ service('ActivityDistributor', 'nsIHttpA
 service('HistoryService', 'mozilla::IHistory',
         "@mozilla.org/browser/history;1")
 service('ThirdPartyUtil', 'mozIThirdPartyUtil',
         "@mozilla.org/thirdpartyutil;1")
 service('URIFixup', 'nsIURIFixup',
         "@mozilla.org/docshell/urifixup;1")
 service('Bits', 'nsIBits',
         "@mozilla.org/bits;1")
+# NB: this should also expose nsIXULAppInfo, as does Services.jsm.
+service('AppInfoService', 'nsIXULRuntime',
+        "@mozilla.org/xre/app-info;1")
 
 # The definition file needs access to the definitions of the particular
 # interfaces. If you add a new interface here, make sure the necessary includes
 # are also listed in the following code snippet.
 CPP_INCLUDES = """
 #include "mozilla/Likely.h"
 #include "mozilla/Services.h"
 #include "mozIThirdPartyUtil.h"
@@ -80,16 +83,17 @@ CPP_INCLUDES = """
 #include "nsISocketTransportService.h"
 #include "nsIURIClassifier.h"
 #include "nsIHttpActivityObserver.h"
 #include "nsIAsyncShutdown.h"
 #include "nsIUUIDGenerator.h"
 #include "nsIGfxInfo.h"
 #include "nsIURIFixup.h"
 #include "nsIBits.h"
+#include "nsIXULRuntime.h"
 """
 
 #####
 # Codegen Logic
 #
 # The following code consumes the data listed above to generate the files
 # Services.h, Services.cpp, and services.rs which provide access to these
 # service getters in both rust and C++ code.
--- a/xpcom/rust/nserror/src/lib.rs
+++ b/xpcom/rust/nserror/src/lib.rs
@@ -47,16 +47,28 @@ impl fmt::Display for nsresult {
 }
 
 impl fmt::Debug for nsresult {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         write!(f, "{}", self.error_name())
     }
 }
 
+impl<T, E> From<Result<T, E>> for nsresult
+where
+    E: Into<nsresult>,
+{
+    fn from(result: Result<T, E>) -> nsresult {
+        match result {
+            Ok(_) => NS_OK,
+            Err(e) => e.into(),
+        }
+    }
+}
+
 impl Error for nsresult {}
 
 extern "C" {
     fn Gecko_GetErrorName(rv: nsresult, cstr: *mut nsACString);
 }
 
 mod error_list {
     include!(concat!(env!("MOZ_TOPOBJDIR"), "/xpcom/base/error_list.rs"));
--- a/xpfe/appshell/nsXULWindow.cpp
+++ b/xpfe/appshell/nsXULWindow.cpp
@@ -50,16 +50,17 @@
 #include "nsGlobalWindow.h"
 #include "XULDocument.h"
 #include "nsXULTooltipListener.h"
 
 #include "prenv.h"
 #include "mozilla/AutoRestore.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/Services.h"
+#include "mozilla/XULStore.h"
 #include "mozilla/dom/BarProps.h"
 #include "mozilla/dom/Element.h"
 #include "mozilla/dom/Event.h"
 #include "mozilla/dom/ScriptSettings.h"
 #include "mozilla/dom/TabParent.h"
 
 using namespace mozilla;
 using dom::AutoNoJSAPI;
@@ -1598,25 +1599,18 @@ nsresult nsXULWindow::GetPersistentValue
   if (!docURI) {
     return NS_ERROR_FAILURE;
   }
   nsAutoCString utf8uri;
   nsresult rv = docURI->GetSpec(utf8uri);
   NS_ENSURE_SUCCESS(rv, rv);
   NS_ConvertUTF8toUTF16 uri(utf8uri);
 
-  if (!mLocalStore) {
-    mLocalStore = do_GetService("@mozilla.org/xul/xulstore;1");
-    if (NS_WARN_IF(!mLocalStore)) {
-      return NS_ERROR_NOT_INITIALIZED;
-    }
-  }
-
-  rv = mLocalStore->GetValue(uri, windowElementId, nsDependentAtomString(aAttr),
-                             aValue);
+  nsDependentAtomString attrString(aAttr);
+  rv = XULStore::GetValue(uri, windowElementId, attrString, aValue);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   if (aAttr == nsGkAtoms::width || aAttr == nsGkAtoms::height) {
     // Convert attributes from outer size to inner size for top-level
     // windows, see bug 1444525 & co.
     ConvertWindowSize(this, aAttr, ConversionDirection::OuterToInner, aValue);
@@ -1656,25 +1650,19 @@ nsresult nsXULWindow::SetPersistentValue
   nsAutoString maybeConvertedValue(aValue);
   if (aAttr == nsGkAtoms::width || aAttr == nsGkAtoms::height) {
     // Make sure we store the <window> attributes as outer window size, see
     // bug 1444525 & co.
     ConvertWindowSize(this, aAttr, ConversionDirection::InnerToOuter,
                       maybeConvertedValue);
   }
 
-  if (!mLocalStore) {
-    mLocalStore = do_GetService("@mozilla.org/xul/xulstore;1");
-    if (NS_WARN_IF(!mLocalStore)) {
-      return NS_ERROR_NOT_INITIALIZED;
-    }
-  }
-
-  return mLocalStore->SetValue(
-      uri, windowElementId, nsDependentAtomString(aAttr), maybeConvertedValue);
+  nsDependentAtomString attrString(aAttr);
+  return XULStore::SetValue(uri, windowElementId, attrString,
+                            maybeConvertedValue);
 }
 
 NS_IMETHODIMP nsXULWindow::SavePersistentAttributes() {
   // can happen when the persistence timer fires at an inopportune time
   // during window shutdown
   if (!mDocShell) return NS_ERROR_FAILURE;
 
   nsCOMPtr<dom::Element> docShellElement = GetWindowDOMElement();
--- a/xpfe/appshell/nsXULWindow.h
+++ b/xpfe/appshell/nsXULWindow.h
@@ -28,17 +28,16 @@
 #include "nsIInterfaceRequestor.h"
 #include "nsIInterfaceRequestorUtils.h"
 #include "nsIXULWindow.h"
 #include "nsIPrompt.h"
 #include "nsIAuthPrompt.h"
 #include "nsIXULBrowserWindow.h"
 #include "nsIWidgetListener.h"
 #include "nsITabParent.h"
-#include "nsIXULStore.h"
 
 namespace mozilla {
 namespace dom {
 class Element;
 }  // namespace dom
 }  // namespace mozilla
 
 class nsAtom;
@@ -190,13 +189,12 @@ class nsXULWindow : public nsIBaseWindow
  private:
   // GetPrimaryTabParentSize is called from xpidl methods and we don't have a
   // good way to annotate those with MOZ_CAN_RUN_SCRIPT yet.  It takes no
   // refcounted args other than "this", and the "this" uses seem ok.
   MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult
   GetPrimaryTabParentSize(int32_t* aWidth, int32_t* aHeight);
   nsresult GetPrimaryContentShellSize(int32_t* aWidth, int32_t* aHeight);
   nsresult SetPrimaryTabParentSize(int32_t aWidth, int32_t aHeight);
-  nsCOMPtr<nsIXULStore> mLocalStore;
 };
 
 NS_DEFINE_STATIC_IID_ACCESSOR(nsXULWindow, NS_XULWINDOW_IMPL_CID)
 #endif /* nsXULWindow_h__ */