Import 'uniffi_core' crate

Request Document: go/android-rust-importing-crates
For CL Reviewers: go/android3p#cl-review
For Build Team: go/ab-third-party-imports
Bug: http://b/330712502
Test: m libuniffi_core

Change-Id: If4e0f1f7b68fb1af2317ae6e20ace48b0a6b8c3a
diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json
new file mode 100644
index 0000000..870d019
--- /dev/null
+++ b/.cargo_vcs_info.json
@@ -0,0 +1,6 @@
+{
+  "git": {
+    "sha1": "d5332be35ef497255f7ce49debfd917f6a1009c7"
+  },
+  "path_in_vcs": "uniffi_core"
+}
\ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..c52aea6
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,191 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "addr2line"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+
+[[package]]
+name = "anyhow"
+version = "1.0.81"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247"
+
+[[package]]
+name = "async-compat"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f68a707c1feb095d8c07f8a65b9f506b117d30af431cab89374357de7c11461b"
+dependencies = [
+ "futures-core",
+ "futures-io",
+ "once_cell",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "backtrace"
+version = "0.3.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837"
+dependencies = [
+ "addr2line",
+ "cc",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+]
+
+[[package]]
+name = "bytes"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
+
+[[package]]
+name = "camino"
+version = "1.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c"
+
+[[package]]
+name = "cc"
+version = "1.0.90"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "futures-core"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
+
+[[package]]
+name = "futures-io"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
+
+[[package]]
+name = "gimli"
+version = "0.28.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
+
+[[package]]
+name = "libc"
+version = "0.2.153"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
+
+[[package]]
+name = "log"
+version = "0.4.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
+
+[[package]]
+name = "memchr"
+version = "2.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7"
+dependencies = [
+ "adler",
+]
+
+[[package]]
+name = "object"
+version = "0.32.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
+
+[[package]]
+name = "oneshot-uniffi"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c548d5c78976f6955d72d0ced18c48ca07030f7a1d4024529fedd7c1c01b29c"
+
+[[package]]
+name = "paste"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "tokio"
+version = "1.36.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931"
+dependencies = [
+ "backtrace",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "uniffi_core"
+version = "0.26.1"
+dependencies = [
+ "anyhow",
+ "async-compat",
+ "bytes",
+ "camino",
+ "log",
+ "once_cell",
+ "oneshot-uniffi",
+ "paste",
+ "static_assertions",
+]
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..0b74865
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,60 @@
+# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
+#
+# When uploading crates to the registry Cargo will automatically
+# "normalize" Cargo.toml files for maximal compatibility
+# with all versions of Cargo and also rewrite `path` dependencies
+# to registry (e.g., crates.io) dependencies.
+#
+# If you are reading this file be aware that the original Cargo.toml
+# will likely look very different (and much more reasonable).
+# See Cargo.toml.orig for the original contents.
+
+[package]
+edition = "2021"
+name = "uniffi_core"
+version = "0.26.1"
+authors = ["Firefox Sync Team <sync-team@mozilla.com>"]
+description = "a multi-language bindings generator for rust (runtime support code)"
+homepage = "https://mozilla.github.io/uniffi-rs"
+documentation = "https://mozilla.github.io/uniffi-rs"
+readme = "README.md"
+keywords = [
+    "ffi",
+    "bindgen",
+]
+license = "MPL-2.0"
+repository = "https://github.com/mozilla/uniffi-rs"
+
+[dependencies.anyhow]
+version = "1"
+
+[dependencies.async-compat]
+version = "0.2.1"
+optional = true
+
+[dependencies.bytes]
+version = "1.3"
+
+[dependencies.camino]
+version = "1.0.8"
+
+[dependencies.log]
+version = "0.4"
+
+[dependencies.once_cell]
+version = "1.10.0"
+
+[dependencies.oneshot]
+version = "0.1.5"
+features = ["async"]
+package = "oneshot-uniffi"
+
+[dependencies.paste]
+version = "1.0"
+
+[dependencies.static_assertions]
+version = "1.1.0"
+
+[features]
+default = []
+tokio = ["dep:async-compat"]
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..a612ad9
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,373 @@
+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+    means each individual or legal entity that creates, contributes to
+    the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+    means the combination of the Contributions of others (if any) used
+    by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+    means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+    means Source Code Form to which the initial Contributor has attached
+    the notice in Exhibit A, the Executable Form of such Source Code
+    Form, and Modifications of such Source Code Form, in each case
+    including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+    means
+
+    (a) that the initial Contributor has attached the notice described
+        in Exhibit B to the Covered Software; or
+
+    (b) that the Covered Software was made available under the terms of
+        version 1.1 or earlier of the License, but not also under the
+        terms of a Secondary License.
+
+1.6. "Executable Form"
+    means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+    means a work that combines Covered Software with other material, in
+    a separate file or files, that is not Covered Software.
+
+1.8. "License"
+    means this document.
+
+1.9. "Licensable"
+    means having the right to grant, to the maximum extent possible,
+    whether at the time of the initial grant or subsequently, any and
+    all of the rights conveyed by this License.
+
+1.10. "Modifications"
+    means any of the following:
+
+    (a) any file in Source Code Form that results from an addition to,
+        deletion from, or modification of the contents of Covered
+        Software; or
+
+    (b) any new file in Source Code Form that contains any Covered
+        Software.
+
+1.11. "Patent Claims" of a Contributor
+    means any patent claim(s), including without limitation, method,
+    process, and apparatus claims, in any patent Licensable by such
+    Contributor that would be infringed, but for the grant of the
+    License, by the making, using, selling, offering for sale, having
+    made, import, or transfer of either its Contributions or its
+    Contributor Version.
+
+1.12. "Secondary License"
+    means either the GNU General Public License, Version 2.0, the GNU
+    Lesser General Public License, Version 2.1, the GNU Affero General
+    Public License, Version 3.0, or any later versions of those
+    licenses.
+
+1.13. "Source Code Form"
+    means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+    means an individual or a legal entity exercising rights under this
+    License. For legal entities, "You" includes any entity that
+    controls, is controlled by, or is under common control with You. For
+    purposes of this definition, "control" means (a) the power, direct
+    or indirect, to cause the direction or management of such entity,
+    whether by contract or otherwise, or (b) ownership of more than
+    fifty percent (50%) of the outstanding shares or beneficial
+    ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+    Licensable by such Contributor to use, reproduce, make available,
+    modify, display, perform, distribute, and otherwise exploit its
+    Contributions, either on an unmodified basis, with Modifications, or
+    as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+    for sale, have made, import, and otherwise transfer either its
+    Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+    or
+
+(b) for infringements caused by: (i) Your and any other third party's
+    modifications of Covered Software, or (ii) the combination of its
+    Contributions with other software (except as part of its Contributor
+    Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+    its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+    Form, as described in Section 3.1, and You must inform recipients of
+    the Executable Form how they can obtain a copy of such Source Code
+    Form by reasonable means in a timely manner, at a charge no more
+    than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+    License, or sublicense it under different terms, provided that the
+    license for the Executable Form does not attempt to limit or alter
+    the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+*                                                                      *
+*  6. Disclaimer of Warranty                                           *
+*  -------------------------                                           *
+*                                                                      *
+*  Covered Software is provided under this License on an "as is"       *
+*  basis, without warranty of any kind, either expressed, implied, or  *
+*  statutory, including, without limitation, warranties that the       *
+*  Covered Software is free of defects, merchantable, fit for a        *
+*  particular purpose or non-infringing. The entire risk as to the     *
+*  quality and performance of the Covered Software is with You.        *
+*  Should any Covered Software prove defective in any respect, You     *
+*  (not any Contributor) assume the cost of any necessary servicing,   *
+*  repair, or correction. This disclaimer of warranty constitutes an   *
+*  essential part of this License. No use of any Covered Software is   *
+*  authorized under this License except under this disclaimer.         *
+*                                                                      *
+************************************************************************
+
+************************************************************************
+*                                                                      *
+*  7. Limitation of Liability                                          *
+*  --------------------------                                          *
+*                                                                      *
+*  Under no circumstances and under no legal theory, whether tort      *
+*  (including negligence), contract, or otherwise, shall any           *
+*  Contributor, or anyone who distributes Covered Software as          *
+*  permitted above, be liable to You for any direct, indirect,         *
+*  special, incidental, or consequential damages of any character      *
+*  including, without limitation, damages for lost profits, loss of    *
+*  goodwill, work stoppage, computer failure or malfunction, or any    *
+*  and all other commercial damages or losses, even if such party      *
+*  shall have been informed of the possibility of such damages. This   *
+*  limitation of liability shall not apply to liability for death or   *
+*  personal injury resulting from such party's negligence to the       *
+*  extent applicable law prohibits such limitation. Some               *
+*  jurisdictions do not allow the exclusion or limitation of           *
+*  incidental or consequential damages, so this exclusion and          *
+*  limitation may not apply to You.                                    *
+*                                                                      *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+  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/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+  This Source Code Form is "Incompatible With Secondary Licenses", as
+  defined by the Mozilla Public License, v. 2.0.
diff --git a/METADATA b/METADATA
new file mode 100644
index 0000000..131ba68
--- /dev/null
+++ b/METADATA
@@ -0,0 +1,20 @@
+name: "uniffi_core"
+description: "a multi-language bindings generator for rust (runtime support code)"
+third_party {
+  identifier {
+    type: "crates.io"
+    value: "uniffi_core"
+  }
+  identifier {
+    type: "Archive"
+    value: "https://static.crates.io/crates/uniffi_core/uniffi_core-0.26.1.crate"
+    primary_source: true
+  }
+  version: "0.26.1"
+  license_type: RECIPROCAL
+  last_upgrade_date {
+    year: 2024
+    month: 4
+    day: 10
+  }
+}
diff --git a/MODULE_LICENSE_MPL b/MODULE_LICENSE_MPL
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/MODULE_LICENSE_MPL
diff --git a/OWNERS b/OWNERS
new file mode 100644
index 0000000..48bea6e
--- /dev/null
+++ b/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 688011
+include platform/prebuilts/rust:main:/OWNERS
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..bb72360
--- /dev/null
+++ b/README.md
@@ -0,0 +1,79 @@
+# UniFFI - a multi-language bindings generator for Rust
+
+UniFFI is a toolkit for building cross-platform software components in Rust.
+
+For the impatient, see [**the UniFFI user guide**](https://mozilla.github.io/uniffi-rs/)
+or [**the UniFFI examples**](https://github.com/mozilla/uniffi-rs/tree/main/examples#example-uniffi-components).
+
+By writing your core business logic in Rust and describing its interface in an "object model",
+you can use UniFFI to help you:
+
+* Compile your Rust code into a shared library for use on different target platforms.
+* Generate bindings to load and use the library from different target languages.
+
+You can describe your object model in an [interface definition file](https://mozilla.github.io/uniffi-rs/udl_file_spec.html)
+or [by using proc-macros](https://mozilla.github.io/uniffi-rs/proc_macro/index.html).
+
+UniFFI is currently extensively by Mozilla in Firefox mobile and desktop browsers;
+written once in Rust, auto-generated bindings allow that functionality to be called
+from both Kotlin (for Android apps) and Swift (for iOS apps).
+It also has a growing community of users shipping various cool things to many users.
+
+UniFII comes with support for **Kotlin**, **Swift**, **Python** and **Ruby** with 3rd party bindings available for **C#** and **Golang**.
+Additional foreign language bindings can be developed externally and we welcome contributions to list them here.
+See [Third-party foreign language bindings](#third-party-foreign-language-bindings).
+
+## User Guide
+
+You can read more about using the tool in [**the UniFFI user guide**](https://mozilla.github.io/uniffi-rs/).
+
+We consider it ready for production use, but UniFFI is a long way from a 1.0 release with lots of internal work still going on.
+We try hard to avoid breaking simple consumers, but more advanced things might break as you upgrade over time.
+
+### Etymology and Pronunciation
+
+ˈjuːnɪfaɪ. Pronounced to rhyme with "unify".
+
+A portmanteau word that also puns with "unify", to signify the joining of one codebase accessed from many languages.
+
+uni - [Latin ūni-, from ūnus, one]
+FFI - [Abbreviation, Foreign Function Interface]
+
+## Alternative tools
+
+Other tools we know of which try and solve a similarly shaped problem are:
+
+* [Diplomat](https://github.com/rust-diplomat/diplomat/) - see our [writeup of
+  the different approach taken by that tool](docs/diplomat-and-macros.md)
+* [Interoptopus](https://github.com/ralfbiedert/interoptopus/)
+
+(Please open a PR if you think other tools should be listed!)
+
+## Third-party foreign language bindings
+
+* [Kotlin Multiplatform support](https://gitlab.com/trixnity/uniffi-kotlin-multiplatform-bindings). The repository contains Kotlin Multiplatform bindings generation for UniFFI, letting you target both JVM and Native.
+* [Go bindings](https://github.com/NordSecurity/uniffi-bindgen-go)
+* [C# bindings](https://github.com/NordSecurity/uniffi-bindgen-cs)
+* [Dart bindings](https://github.com/NiallBunting/uniffi-rs-dart)
+
+### External resources
+
+There are a few third-party resources that make it easier to work with UniFFI:
+
+* [Plugin support for `.udl` files](https://github.com/Lonami/uniffi-dl) for the IDEA platform ([*uniffi-dl* in the JetBrains marketplace](https://plugins.jetbrains.com/plugin/20527-uniffi-dl)). It provides syntax highlighting, code folding, code completion, reference resolution and navigation (among others features) for the [UniFFI Definition Language (UDL)](https://mozilla.github.io/uniffi-rs/).
+* [cargo swift](https://github.com/antoniusnaumann/cargo-swift), a cargo plugin to build a Swift Package from Rust code. It provides an init command for setting up a UniFFI crate and a package command for building a Swift package from Rust code - without the need for additional configuration or build scripts.
+
+(Please open a PR if you think other resources should be listed!)
+
+## Contributing
+
+If this tool sounds interesting to you, please help us develop it! You can:
+
+* View the [contributor guidelines](./docs/contributing.md).
+* File or work on [issues](https://github.com/mozilla/uniffi-rs/issues) here in GitHub.
+* Join discussions in the [#uniffi:mozilla.org](https://matrix.to/#/#uniffi:mozilla.org)
+  room on Matrix.
+
+## Code of Conduct
+
+This project is governed by Mozilla's [Community Participation Guidelines](./CODE_OF_CONDUCT.md).
diff --git a/cargo_embargo.json b/cargo_embargo.json
new file mode 100644
index 0000000..9a0a579
--- /dev/null
+++ b/cargo_embargo.json
@@ -0,0 +1,3 @@
+{
+  "tests": true
+}
diff --git a/release.toml b/release.toml
new file mode 100644
index 0000000..2ff9c83
--- /dev/null
+++ b/release.toml
@@ -0,0 +1,15 @@
+# Note that this `release.toml` exists to capture things that must only be
+# done once for `cargo release-backend-crates`.
+#
+# [../uniffi/release.toml](../uniffi/release.toml) captures things that must only be done for `cargo release-uniffi`
+#
+# All other config exists in [../release.toml](../release.toml).
+
+tag = false
+
+# This is how we manage the sections in CHANGELOG.md
+pre-release-replacements = [
+  {file="../CHANGELOG.md", search="\\[\\[UnreleasedBackendVersion\\]\\]", replace="v{{version}}", exactly=1},
+  {file="../CHANGELOG.md", search="\\[\\[ReleaseDate\\]\\]", replace="{{date}}", exactly=1},
+  {file="../CHANGELOG.md", search="<!-- next-header -->", replace="<!-- next-header -->\n\n## [[NextUnreleasedUniFFIVersion]] (backend crates: [[UnreleasedBackendVersion]]) - (_[[ReleaseDate]]_)\n\n[All changes in [[NextUnreleasedUniFFIVersion]]](https://github.com/mozilla/uniffi-rs/compare/v{{version}}...NEXT_HEAD).", exactly=1},
+]
diff --git a/src/ffi/callbackinterface.rs b/src/ffi/callbackinterface.rs
new file mode 100644
index 0000000..41c85dc
--- /dev/null
+++ b/src/ffi/callbackinterface.rs
@@ -0,0 +1,309 @@
+/* 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/. */
+
+//! Callback interfaces are traits specified in UDL which can be implemented by foreign languages.
+//!
+//! # Using callback interfaces
+//!
+//! 1. Define a Rust trait.
+//!
+//! This toy example defines a way of Rust accessing a key-value store exposed
+//! by the host operating system (e.g. the key chain).
+//!
+//! ```
+//! trait Keychain: Send {
+//!   fn get(&self, key: String) -> Option<String>;
+//!   fn put(&self, key: String, value: String);
+//! }
+//! ```
+//!
+//! 2. Define a callback interface in the UDL
+//!
+//! ```idl
+//! callback interface Keychain {
+//!     string? get(string key);
+//!     void put(string key, string data);
+//! };
+//! ```
+//!
+//! 3. And allow it to be passed into Rust.
+//!
+//! Here, we define a constructor to pass the keychain to rust, and then another method
+//! which may use it.
+//!
+//! In UDL:
+//! ```idl
+//! object Authenticator {
+//!     constructor(Keychain keychain);
+//!     void login();
+//! }
+//! ```
+//!
+//! In Rust:
+//!
+//! ```
+//!# trait Keychain: Send {
+//!#  fn get(&self, key: String) -> Option<String>;
+//!#  fn put(&self, key: String, value: String);
+//!# }
+//! struct Authenticator {
+//!   keychain: Box<dyn Keychain>,
+//! }
+//!
+//! impl Authenticator {
+//!   pub fn new(keychain: Box<dyn Keychain>) -> Self {
+//!     Self { keychain }
+//!   }
+//!   pub fn login(&self) {
+//!     let username = self.keychain.get("username".into());
+//!     let password = self.keychain.get("password".into());
+//!   }
+//! }
+//! ```
+//! 4. Create an foreign language implementation of the callback interface.
+//!
+//! In this example, here's a Kotlin implementation.
+//!
+//! ```kotlin
+//! class AndroidKeychain: Keychain {
+//!     override fun get(key: String): String? {
+//!         // … elide the implementation.
+//!         return value
+//!     }
+//!     override fun put(key: String) {
+//!         // … elide the implementation.
+//!     }
+//! }
+//! ```
+//! 5. Pass the implementation to Rust.
+//!
+//! Again, in Kotlin
+//!
+//! ```kotlin
+//! val authenticator = Authenticator(AndroidKeychain())
+//! authenticator.login()
+//! ```
+//!
+//! # How it works.
+//!
+//! ## High level
+//!
+//! Uniffi generates a protocol or interface in client code in the foreign language must implement.
+//!
+//! For each callback interface, a `CallbackInternals` (on the Foreign Language side) and `ForeignCallbackInternals`
+//! (on Rust side) manages the process through a `ForeignCallback`. There is one `ForeignCallback` per callback interface.
+//!
+//! Passing a callback interface implementation from foreign language (e.g. `AndroidKeychain`) into Rust causes the
+//! `KeychainCallbackInternals` to store the instance in a handlemap.
+//!
+//! The object handle is passed over to Rust, and used to instantiate a struct `KeychainProxy` which implements
+//! the trait. This proxy implementation is generate by Uniffi. The `KeychainProxy` object is then passed to
+//! client code as `Box<dyn Keychain>`.
+//!
+//! Methods on `KeychainProxy` objects (e.g. `self.keychain.get("username".into())`) encode the arguments into a `RustBuffer`.
+//! Using the `ForeignCallback`, it calls the `CallbackInternals` object on the foreign language side using the
+//! object handle, and the method selector.
+//!
+//! The `CallbackInternals` object unpacks the arguments from the passed buffer, gets the object out from the handlemap,
+//! and calls the actual implementation of the method.
+//!
+//! If there's a return value, it is packed up in to another `RustBuffer` and used as the return value for
+//! `ForeignCallback`. The caller of `ForeignCallback`, the `KeychainProxy` unpacks the returned buffer into the correct
+//! type and then returns to client code.
+//!
+
+use crate::{ForeignCallback, ForeignCallbackCell, Lift, LiftReturn, RustBuffer};
+use std::fmt;
+
+/// The method index used by the Drop trait to communicate to the foreign language side that Rust has finished with it,
+/// and it can be deleted from the handle map.
+pub const IDX_CALLBACK_FREE: u32 = 0;
+
+/// Result of a foreign callback invocation
+#[repr(i32)]
+#[derive(Debug, PartialEq, Eq)]
+pub enum CallbackResult {
+    /// Successful call.
+    /// The return value is serialized to `buf_ptr`.
+    Success = 0,
+    /// Expected error.
+    /// This is returned when a foreign method throws an exception that corresponds to the Rust Err half of a Result.
+    /// The error value is serialized to `buf_ptr`.
+    Error = 1,
+    /// Unexpected error.
+    /// An error message string is serialized to `buf_ptr`.
+    UnexpectedError = 2,
+}
+
+impl TryFrom<i32> for CallbackResult {
+    // On errors we return the unconverted value
+    type Error = i32;
+
+    fn try_from(value: i32) -> Result<Self, i32> {
+        match value {
+            0 => Ok(Self::Success),
+            1 => Ok(Self::Error),
+            2 => Ok(Self::UnexpectedError),
+            n => Err(n),
+        }
+    }
+}
+
+/// Struct to hold a foreign callback.
+pub struct ForeignCallbackInternals {
+    callback_cell: ForeignCallbackCell,
+}
+
+impl ForeignCallbackInternals {
+    pub const fn new() -> Self {
+        ForeignCallbackInternals {
+            callback_cell: ForeignCallbackCell::new(),
+        }
+    }
+
+    pub fn set_callback(&self, callback: ForeignCallback) {
+        self.callback_cell.set(callback);
+    }
+
+    /// Invoke a callback interface method on the foreign side and return the result
+    pub fn invoke_callback<R, UniFfiTag>(&self, handle: u64, method: u32, args: RustBuffer) -> R
+    where
+        R: LiftReturn<UniFfiTag>,
+    {
+        let mut ret_rbuf = RustBuffer::new();
+        let callback = self.callback_cell.get();
+        let raw_result = unsafe {
+            callback(
+                handle,
+                method,
+                args.data_pointer(),
+                args.len() as i32,
+                &mut ret_rbuf,
+            )
+        };
+        RustBuffer::destroy(args);
+        let result = CallbackResult::try_from(raw_result)
+            .unwrap_or_else(|code| panic!("Callback failed with unexpected return code: {code}"));
+        match result {
+            CallbackResult::Success => R::lift_callback_return(ret_rbuf),
+            CallbackResult::Error => R::lift_callback_error(ret_rbuf),
+            CallbackResult::UnexpectedError => {
+                let reason = if !ret_rbuf.is_empty() {
+                    match <String as Lift<UniFfiTag>>::try_lift(ret_rbuf) {
+                        Ok(s) => s,
+                        Err(e) => {
+                            log::error!("{{ trait_name }} Error reading ret_buf: {e}");
+                            String::from("[Error reading reason]")
+                        }
+                    }
+                } else {
+                    RustBuffer::destroy(ret_rbuf);
+                    String::from("[Unknown Reason]")
+                };
+                R::handle_callback_unexpected_error(UnexpectedUniFFICallbackError { reason })
+            }
+        }
+    }
+}
+
+/// Used when internal/unexpected error happened when calling a foreign callback, for example when
+/// a unknown exception is raised
+///
+/// User callback error types must implement a From impl from this type to their own error type.
+#[derive(Debug)]
+pub struct UnexpectedUniFFICallbackError {
+    pub reason: String,
+}
+
+impl UnexpectedUniFFICallbackError {
+    pub fn from_reason(reason: String) -> Self {
+        Self { reason }
+    }
+}
+
+impl fmt::Display for UnexpectedUniFFICallbackError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(
+            f,
+            "UnexpectedUniFFICallbackError(reason: {:?})",
+            self.reason
+        )
+    }
+}
+
+impl std::error::Error for UnexpectedUniFFICallbackError {}
+
+// Autoref-based specialization for converting UnexpectedUniFFICallbackError into error types.
+//
+// For more details, see:
+// https://github.com/dtolnay/case-studies/blob/master/autoref-specialization/README.md
+
+// Define two ZST types:
+//   - One implements `try_convert_unexpected_callback_error` by always returning an error value.
+//   - The specialized version implements it using `From<UnexpectedUniFFICallbackError>`
+
+#[doc(hidden)]
+#[derive(Debug)]
+pub struct UnexpectedUniFFICallbackErrorConverterGeneric;
+
+impl UnexpectedUniFFICallbackErrorConverterGeneric {
+    pub fn try_convert_unexpected_callback_error<E>(
+        &self,
+        e: UnexpectedUniFFICallbackError,
+    ) -> anyhow::Result<E> {
+        Err(e.into())
+    }
+}
+
+#[doc(hidden)]
+#[derive(Debug)]
+pub struct UnexpectedUniFFICallbackErrorConverterSpecialized;
+
+impl UnexpectedUniFFICallbackErrorConverterSpecialized {
+    pub fn try_convert_unexpected_callback_error<E>(
+        &self,
+        e: UnexpectedUniFFICallbackError,
+    ) -> anyhow::Result<E>
+    where
+        E: From<UnexpectedUniFFICallbackError>,
+    {
+        Ok(E::from(e))
+    }
+}
+
+// Macro to convert an UnexpectedUniFFICallbackError value for a particular type.  This is used in
+// the `ConvertError` implementation.
+#[doc(hidden)]
+#[macro_export]
+macro_rules! convert_unexpected_error {
+    ($error:ident, $ty:ty) => {{
+        // Trait for generic conversion, implemented for all &T.
+        pub trait GetConverterGeneric {
+            fn get_converter(&self) -> $crate::UnexpectedUniFFICallbackErrorConverterGeneric;
+        }
+
+        impl<T> GetConverterGeneric for &T {
+            fn get_converter(&self) -> $crate::UnexpectedUniFFICallbackErrorConverterGeneric {
+                $crate::UnexpectedUniFFICallbackErrorConverterGeneric
+            }
+        }
+        // Trait for specialized conversion, implemented for all T that implements
+        // `Into<ErrorType>`.  I.e. it's implemented for UnexpectedUniFFICallbackError when
+        // ErrorType implements From<UnexpectedUniFFICallbackError>.
+        pub trait GetConverterSpecialized {
+            fn get_converter(&self) -> $crate::UnexpectedUniFFICallbackErrorConverterSpecialized;
+        }
+
+        impl<T: Into<$ty>> GetConverterSpecialized for T {
+            fn get_converter(&self) -> $crate::UnexpectedUniFFICallbackErrorConverterSpecialized {
+                $crate::UnexpectedUniFFICallbackErrorConverterSpecialized
+            }
+        }
+        // Here's the hack.  Because of the auto-ref rules, this will use `GetConverterSpecialized`
+        // if it's implemented and `GetConverterGeneric` if not.
+        (&$error)
+            .get_converter()
+            .try_convert_unexpected_callback_error($error)
+    }};
+}
diff --git a/src/ffi/ffidefault.rs b/src/ffi/ffidefault.rs
new file mode 100644
index 0000000..d3ca943
--- /dev/null
+++ b/src/ffi/ffidefault.rs
@@ -0,0 +1,58 @@
+/* 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/. */
+
+//! FfiDefault trait
+//!
+//! When we make a FFI call into Rust we always need to return a value, even if that value will be
+//! ignored because we're flagging an exception.  This trait defines what that value is for our
+//! supported FFI types.
+
+use paste::paste;
+
+pub trait FfiDefault {
+    fn ffi_default() -> Self;
+}
+
+// Most types can be handled by delegating to Default
+macro_rules! impl_ffi_default_with_default {
+    ($($T:ty,)+) => { impl_ffi_default_with_default!($($T),+); };
+    ($($T:ty),*) => {
+            $(
+                paste! {
+                    impl FfiDefault for $T {
+                        fn ffi_default() -> Self {
+                            $T::default()
+                        }
+                    }
+                }
+            )*
+    };
+}
+
+impl_ffi_default_with_default! {
+    bool, i8, u8, i16, u16, i32, u32, i64, u64, f32, f64
+}
+
+// Implement FfiDefault for the remaining types
+impl FfiDefault for () {
+    fn ffi_default() {}
+}
+
+impl FfiDefault for *const std::ffi::c_void {
+    fn ffi_default() -> Self {
+        std::ptr::null()
+    }
+}
+
+impl FfiDefault for crate::RustBuffer {
+    fn ffi_default() -> Self {
+        unsafe { Self::from_raw_parts(std::ptr::null_mut(), 0, 0) }
+    }
+}
+
+impl<T> FfiDefault for Option<T> {
+    fn ffi_default() -> Self {
+        None
+    }
+}
diff --git a/src/ffi/foreignbytes.rs b/src/ffi/foreignbytes.rs
new file mode 100644
index 0000000..9516f61
--- /dev/null
+++ b/src/ffi/foreignbytes.rs
@@ -0,0 +1,118 @@
+/* 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/. */
+
+/// Support for reading a slice of foreign-language-allocated bytes over the FFI.
+///
+/// Foreign language code can pass a slice of bytes by providing a data pointer
+/// and length, and this struct provides a convenient wrapper for working with
+/// that pair. Naturally, this can be tremendously unsafe! So here are the details:
+///
+///   * The foreign language code must ensure the provided buffer stays alive
+///     and unchanged for the duration of the call to which the `ForeignBytes`
+///     struct was provided.
+///
+/// To work with the bytes in Rust code, use `as_slice()` to view the data
+/// as a `&[u8]`.
+///
+/// Implementation note: all the fields of this struct are private and it has no
+/// constructors, so consuming crates cant create instances of it. If you've
+/// got a `ForeignBytes`, then you received it over the FFI and are assuming that
+/// the foreign language code is upholding the above invariants.
+///
+/// This struct is based on `ByteBuffer` from the `ffi-support` crate, but modified
+/// to give a read-only view of externally-provided bytes.
+#[repr(C)]
+pub struct ForeignBytes {
+    /// The length of the pointed-to data.
+    /// We use an `i32` for compatibility with JNA.
+    len: i32,
+    /// The pointer to the foreign-owned bytes.
+    data: *const u8,
+}
+
+impl ForeignBytes {
+    /// Creates a `ForeignBytes` from its constituent fields.
+    ///
+    /// This is intended mainly as an internal convenience function and should not
+    /// be used outside of this module.
+    ///
+    /// # Safety
+    ///
+    /// You must ensure that the raw parts uphold the documented invariants of this class.
+    pub unsafe fn from_raw_parts(data: *const u8, len: i32) -> Self {
+        Self { len, data }
+    }
+
+    /// View the foreign bytes as a `&[u8]`.
+    ///
+    /// # Panics
+    ///
+    /// Panics if the provided struct has a null pointer but non-zero length.
+    /// Panics if the provided length is negative.
+    pub fn as_slice(&self) -> &[u8] {
+        if self.data.is_null() {
+            assert!(self.len == 0, "null ForeignBytes had non-zero length");
+            &[]
+        } else {
+            unsafe { std::slice::from_raw_parts(self.data, self.len()) }
+        }
+    }
+
+    /// Get the length of this slice of bytes.
+    ///
+    /// # Panics
+    ///
+    /// Panics if the provided length is negative.
+    pub fn len(&self) -> usize {
+        self.len
+            .try_into()
+            .expect("bytes length negative or overflowed")
+    }
+
+    /// Returns true if the length of this slice of bytes is 0.
+    pub fn is_empty(&self) -> bool {
+        self.len == 0
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+    #[test]
+    fn test_foreignbytes_access() {
+        let v = [1u8, 2, 3];
+        let fbuf = unsafe { ForeignBytes::from_raw_parts(v.as_ptr(), 3) };
+        assert_eq!(fbuf.len(), 3);
+        assert_eq!(fbuf.as_slice(), &[1u8, 2, 3]);
+    }
+
+    #[test]
+    fn test_foreignbytes_empty() {
+        let v = Vec::<u8>::new();
+        let fbuf = unsafe { ForeignBytes::from_raw_parts(v.as_ptr(), 0) };
+        assert_eq!(fbuf.len(), 0);
+        assert_eq!(fbuf.as_slice(), &[0u8; 0]);
+    }
+
+    #[test]
+    fn test_foreignbytes_null_means_empty() {
+        let fbuf = unsafe { ForeignBytes::from_raw_parts(std::ptr::null_mut(), 0) };
+        assert_eq!(fbuf.as_slice(), &[0u8; 0]);
+    }
+
+    #[test]
+    #[should_panic]
+    fn test_foreignbytes_null_must_have_zero_length() {
+        let fbuf = unsafe { ForeignBytes::from_raw_parts(std::ptr::null_mut(), 12) };
+        fbuf.as_slice();
+    }
+
+    #[test]
+    #[should_panic]
+    fn test_foreignbytes_provided_len_must_be_non_negative() {
+        let v = [0u8, 1, 2];
+        let fbuf = unsafe { ForeignBytes::from_raw_parts(v.as_ptr(), -1) };
+        fbuf.as_slice();
+    }
+}
diff --git a/src/ffi/foreigncallbacks.rs b/src/ffi/foreigncallbacks.rs
new file mode 100644
index 0000000..68d9a0d
--- /dev/null
+++ b/src/ffi/foreigncallbacks.rs
@@ -0,0 +1,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/. */
+
+//! This module contains code to handle foreign callbacks - C-ABI functions that are defined by a
+//! foreign language, then registered with UniFFI.  These callbacks are used to implement callback
+//! interfaces, async scheduling etc. Foreign callbacks are registered at startup, when the foreign
+//! code loads the exported library. For each callback type, we also define a "cell" type for
+//! storing the callback.
+
+use std::sync::atomic::{AtomicUsize, Ordering};
+
+use crate::RustBuffer;
+
+/// ForeignCallback is the Rust representation of a foreign language function.
+/// It is the basis for all callbacks interfaces. It is registered exactly once per callback interface,
+/// at library start up time.
+/// Calling this method is only done by generated objects which mirror callback interfaces objects in the foreign language.
+///
+/// * The `handle` is the key into a handle map on the other side of the FFI used to look up the foreign language object
+///   that implements the callback interface/trait.
+/// * The `method` selector specifies the method that will be called on the object, by looking it up in a list of methods from
+///   the IDL. The list is 1 indexed. Note that the list of methods is generated by UniFFI from the IDL and used in all
+///   bindings, so we can rely on the method list being stable within the same run of UniFFI.
+/// * `args_data` and `args_len` represents a serialized buffer of arguments to the function. The scaffolding code
+///   writes the callback arguments to this buffer, in order, using `FfiConverter.write()`. The bindings code reads the
+///   arguments from the buffer and passes them to the user's callback.
+/// * `buf_ptr` is a pointer to where the resulting buffer will be written. UniFFI will allocate a
+///   buffer to write the result into.
+/// * Callbacks return one of the `CallbackResult` values
+///   Note: The output buffer might still contain 0 bytes of data.
+pub type ForeignCallback = unsafe extern "C" fn(
+    handle: u64,
+    method: u32,
+    args_data: *const u8,
+    args_len: i32,
+    buf_ptr: *mut RustBuffer,
+) -> i32;
+
+/// Store a [ForeignCallback] pointer
+pub(crate) struct ForeignCallbackCell(AtomicUsize);
+
+/// Macro to define foreign callback types as well as the callback cell.
+macro_rules! impl_foreign_callback_cell {
+    ($callback_type:ident, $cell_type:ident) => {
+        // Overly-paranoid sanity checking to ensure that these types are
+        // convertible between each-other. `transmute` actually should check this for
+        // us too, but this helps document the invariants we rely on in this code.
+        //
+        // Note that these are guaranteed by
+        // https://rust-lang.github.io/unsafe-code-guidelines/layout/function-pointers.html
+        // and thus this is a little paranoid.
+        static_assertions::assert_eq_size!(usize, $callback_type);
+        static_assertions::assert_eq_size!(usize, Option<$callback_type>);
+
+        impl $cell_type {
+            pub const fn new() -> Self {
+                Self(AtomicUsize::new(0))
+            }
+
+            pub fn set(&self, callback: $callback_type) {
+                // Store the pointer using Ordering::Relaxed.  This is sufficient since callback
+                // should be set at startup, before there's any chance of using them.
+                self.0.store(callback as usize, Ordering::Relaxed);
+            }
+
+            pub fn get(&self) -> $callback_type {
+                let ptr_value = self.0.load(Ordering::Relaxed);
+                unsafe {
+                    // SAFETY: self.0 was set in `set` from our function pointer type, so
+                    // it's safe to transmute it back here.
+                    ::std::mem::transmute::<usize, Option<$callback_type>>(ptr_value)
+                        .expect("Bug: callback not set.  This is likely a uniffi bug.")
+                }
+            }
+        }
+    };
+}
+
+impl_foreign_callback_cell!(ForeignCallback, ForeignCallbackCell);
diff --git a/src/ffi/mod.rs b/src/ffi/mod.rs
new file mode 100644
index 0000000..24b9bba
--- /dev/null
+++ b/src/ffi/mod.rs
@@ -0,0 +1,21 @@
+/* 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/. */
+
+//! Types that can cross the FFI boundary.
+
+pub mod callbackinterface;
+pub mod ffidefault;
+pub mod foreignbytes;
+pub mod foreigncallbacks;
+pub mod rustbuffer;
+pub mod rustcalls;
+pub mod rustfuture;
+
+pub use callbackinterface::*;
+pub use ffidefault::FfiDefault;
+pub use foreignbytes::*;
+pub use foreigncallbacks::*;
+pub use rustbuffer::*;
+pub use rustcalls::*;
+pub use rustfuture::*;
diff --git a/src/ffi/rustbuffer.rs b/src/ffi/rustbuffer.rs
new file mode 100644
index 0000000..cfcc542
--- /dev/null
+++ b/src/ffi/rustbuffer.rs
@@ -0,0 +1,355 @@
+/* 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::ffi::{rust_call, ForeignBytes, RustCallStatus};
+
+/// Support for passing an allocated-by-Rust buffer of bytes over the FFI.
+///
+/// We can pass a `Vec<u8>` to foreign language code by decomposing it into
+/// its raw parts (buffer pointer, length, and capacity) and passing those
+/// around as a struct. Naturally, this can be tremendously unsafe! So here
+/// are the details:
+///
+///   * `RustBuffer` structs must only ever be constructed from a `Vec<u8>`,
+///     either explicitly via `RustBuffer::from_vec` or indirectly by calling
+///     one of the `RustBuffer::new*` constructors.
+///
+///   * `RustBuffer` structs do not implement `Drop`, since they are intended
+///     to be passed to foreign-language code outside of the control of Rust's
+///     ownership system. To avoid memory leaks they *must* passed back into
+///     Rust and either explicitly destroyed using `RustBuffer::destroy`, or
+///     converted back to a `Vec<u8>` using `RustBuffer::destroy_into_vec`
+///     (which will then be dropped via Rust's usual ownership-tracking system).
+///
+/// Foreign-language code should not construct `RustBuffer` structs other than
+/// by receiving them from a call into the Rust code, and should not modify them
+/// apart from the following safe operations:
+///
+///   * Writing bytes into the buffer pointed to by `data`, without writing
+///     beyond the indicated `capacity`.
+///
+///   * Adjusting the `len` property to indicate the amount of data written,
+///     while ensuring that 0 <= `len` <= `capacity`.
+///
+///   * As a special case, constructing a `RustBuffer` with zero capacity, zero
+///     length, and a null `data` pointer to indicate an empty buffer.
+///
+/// In particular, it is not safe for foreign-language code to construct a `RustBuffer`
+/// that points to its own allocated memory; use the `ForeignBytes` struct to
+/// pass a view of foreign-owned memory in to Rust code.
+///
+/// Implementation note: all the fields of this struct are private, so you can't
+/// manually construct instances that don't come from a `Vec<u8>`. If you've got
+/// a `RustBuffer` then it either came from a public constructor (all of which
+/// are safe) or it came from foreign-language code (which should have in turn
+/// received it by calling some Rust function, and should be respecting the
+/// invariants listed above).
+///
+/// This struct is based on `ByteBuffer` from the `ffi-support` crate, but modified
+/// to retain unallocated capacity rather than truncating to the occupied length.
+#[repr(C)]
+#[derive(Debug)]
+pub struct RustBuffer {
+    /// The allocated capacity of the underlying `Vec<u8>`.
+    /// In Rust this is a `usize`, but we use an `i32` for compatibility with JNA.
+    capacity: i32,
+    /// The occupied length of the underlying `Vec<u8>`.
+    /// In Rust this is a `usize`, but we use an `i32` for compatibility with JNA.
+    len: i32,
+    /// The pointer to the allocated buffer of the `Vec<u8>`.
+    data: *mut u8,
+}
+
+// Mark `RustBuffer` as safe to send between threads, despite the `u8` pointer.  The only mutable
+// use of that pointer is in `destroy_into_vec()` which requires a &mut on the `RustBuffer`.  This
+// is required to send `RustBuffer` inside a `RustFuture`
+unsafe impl Send for RustBuffer {}
+
+impl RustBuffer {
+    /// Creates an empty `RustBuffer`.
+    ///
+    /// The buffer will not allocate.
+    /// The resulting vector will not be automatically dropped; you must
+    /// arrange to call `destroy` or `destroy_into_vec` when finished with it.
+    pub fn new() -> Self {
+        Self::from_vec(Vec::new())
+    }
+
+    /// Creates a `RustBuffer` from its constituent fields.
+    ///
+    /// This is intended mainly as an internal convenience function and should not
+    /// be used outside of this module.
+    ///
+    /// # Safety
+    ///
+    /// You must ensure that the raw parts uphold the documented invariants of this class.
+    pub unsafe fn from_raw_parts(data: *mut u8, len: i32, capacity: i32) -> Self {
+        Self {
+            capacity,
+            len,
+            data,
+        }
+    }
+
+    /// Get the current length of the buffer, as a `usize`.
+    ///
+    /// This is mostly a helper function to convert the `i32` length field
+    /// into a `usize`, which is what Rust code usually expects.
+    ///
+    /// # Panics
+    ///
+    /// Panics if called on an invalid struct obtained from foreign-language code,
+    /// in which the `len` field is negative.
+    pub fn len(&self) -> usize {
+        self.len
+            .try_into()
+            .expect("buffer length negative or overflowed")
+    }
+
+    /// Get a pointer to the data
+    pub fn data_pointer(&self) -> *const u8 {
+        self.data
+    }
+
+    /// Returns true if the length of the buffer is 0.
+    pub fn is_empty(&self) -> bool {
+        self.len == 0
+    }
+
+    /// Creates a `RustBuffer` zero-filed to the requested size.
+    ///
+    /// The resulting vector will not be automatically dropped; you must
+    /// arrange to call `destroy` or `destroy_into_vec` when finished with it.
+    ///
+    /// # Panics
+    ///
+    /// Panics if the requested size is too large to fit in an `i32`, and
+    /// hence would risk incompatibility with some foreign-language code.
+    pub fn new_with_size(size: usize) -> Self {
+        assert!(
+            size < i32::MAX as usize,
+            "RustBuffer requested size too large"
+        );
+        Self::from_vec(vec![0u8; size])
+    }
+
+    /// Consumes a `Vec<u8>` and returns its raw parts as a `RustBuffer`.
+    ///
+    /// The resulting vector will not be automatically dropped; you must
+    /// arrange to call `destroy` or `destroy_into_vec` when finished with it.
+    ///
+    /// # Panics
+    ///
+    /// Panics if the vector's length or capacity are too large to fit in an `i32`,
+    /// and hence would risk incompatibility with some foreign-language code.
+    pub fn from_vec(v: Vec<u8>) -> Self {
+        let capacity = i32::try_from(v.capacity()).expect("buffer capacity cannot fit into a i32.");
+        let len = i32::try_from(v.len()).expect("buffer length cannot fit into a i32.");
+        let mut v = std::mem::ManuallyDrop::new(v);
+        unsafe { Self::from_raw_parts(v.as_mut_ptr(), len, capacity) }
+    }
+
+    /// Converts this `RustBuffer` back into an owned `Vec<u8>`.
+    ///
+    /// This restores ownership of the underlying buffer to Rust, meaning it will
+    /// be dropped when the `Vec<u8>` is dropped. The `RustBuffer` *must* have been
+    /// previously obtained from a valid `Vec<u8>` owned by this Rust code.
+    ///
+    /// # Panics
+    ///
+    /// Panics if called on an invalid struct obtained from foreign-language code,
+    /// which does not respect the invairiants on `len` and `capacity`.
+    pub fn destroy_into_vec(self) -> Vec<u8> {
+        // Rust will never give us a null `data` pointer for a `Vec`, but
+        // foreign-language code can use it to cheaply pass an empty buffer.
+        if self.data.is_null() {
+            assert!(self.capacity == 0, "null RustBuffer had non-zero capacity");
+            assert!(self.len == 0, "null RustBuffer had non-zero length");
+            vec![]
+        } else {
+            let capacity: usize = self
+                .capacity
+                .try_into()
+                .expect("buffer capacity negative or overflowed");
+            let len: usize = self
+                .len
+                .try_into()
+                .expect("buffer length negative or overflowed");
+            assert!(len <= capacity, "RustBuffer length exceeds capacity");
+            unsafe { Vec::from_raw_parts(self.data, len, capacity) }
+        }
+    }
+
+    /// Reclaim memory stored in this `RustBuffer`.
+    ///
+    /// # Panics
+    ///
+    /// Panics if called on an invalid struct obtained from foreign-language code,
+    /// which does not respect the invairiants on `len` and `capacity`.
+    pub fn destroy(self) {
+        drop(self.destroy_into_vec());
+    }
+}
+
+impl Default for RustBuffer {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+// Functions for the RustBuffer functionality.
+//
+// The scaffolding code re-exports these functions, prefixed with the component name and UDL hash
+// This creates a separate set of functions for each UniFFIed component, which is needed in the
+// case where we create multiple dylib artifacts since each dylib will have its own allocator.
+
+/// This helper allocates a new byte buffer owned by the Rust code, and returns it
+/// to the foreign-language code as a `RustBuffer` struct. Callers must eventually
+/// free the resulting buffer, either by explicitly calling [`uniffi_rustbuffer_free`] defined
+/// below, or by passing ownership of the buffer back into Rust code.
+pub fn uniffi_rustbuffer_alloc(size: i32, call_status: &mut RustCallStatus) -> RustBuffer {
+    rust_call(call_status, || {
+        Ok(RustBuffer::new_with_size(size.max(0) as usize))
+    })
+}
+
+/// This helper copies bytes owned by the foreign-language code into a new byte buffer owned
+/// by the Rust code, and returns it as a `RustBuffer` struct. Callers must eventually
+/// free the resulting buffer, either by explicitly calling the destructor defined below,
+/// or by passing ownership of the buffer back into Rust code.
+///
+/// # Safety
+/// This function will dereference a provided pointer in order to copy bytes from it, so
+/// make sure the `ForeignBytes` struct contains a valid pointer and length.
+pub fn uniffi_rustbuffer_from_bytes(
+    bytes: ForeignBytes,
+    call_status: &mut RustCallStatus,
+) -> RustBuffer {
+    rust_call(call_status, || {
+        let bytes = bytes.as_slice();
+        Ok(RustBuffer::from_vec(bytes.to_vec()))
+    })
+}
+
+/// Free a byte buffer that had previously been passed to the foreign language code.
+///
+/// # Safety
+/// The argument *must* be a uniquely-owned `RustBuffer` previously obtained from a call
+/// into the Rust code that returned a buffer, or you'll risk freeing unowned memory or
+/// corrupting the allocator state.
+pub fn uniffi_rustbuffer_free(buf: RustBuffer, call_status: &mut RustCallStatus) {
+    rust_call(call_status, || {
+        RustBuffer::destroy(buf);
+        Ok(())
+    })
+}
+
+/// Reserve additional capacity in a byte buffer that had previously been passed to the
+/// foreign language code.
+///
+/// The first argument *must* be a uniquely-owned `RustBuffer` previously
+/// obtained from a call into the Rust code that returned a buffer. Its underlying data pointer
+/// will be reallocated if necessary and returned in a new `RustBuffer` struct.
+///
+/// The second argument must be the minimum number of *additional* bytes to reserve
+/// capacity for in the buffer; it is likely to reserve additional capacity in practice
+/// due to amortized growth strategy of Rust vectors.
+///
+/// # Safety
+/// The first argument *must* be a uniquely-owned `RustBuffer` previously obtained from a call
+/// into the Rust code that returned a buffer, or you'll risk freeing unowned memory or
+/// corrupting the allocator state.
+pub fn uniffi_rustbuffer_reserve(
+    buf: RustBuffer,
+    additional: i32,
+    call_status: &mut RustCallStatus,
+) -> RustBuffer {
+    rust_call(call_status, || {
+        let additional: usize = additional
+            .try_into()
+            .expect("additional buffer length negative or overflowed");
+        let mut v = buf.destroy_into_vec();
+        v.reserve(additional);
+        Ok(RustBuffer::from_vec(v))
+    })
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+    #[test]
+    fn test_rustbuffer_from_vec() {
+        let rbuf = RustBuffer::from_vec(vec![1u8, 2, 3]);
+        assert_eq!(rbuf.len(), 3);
+        assert_eq!(rbuf.destroy_into_vec(), vec![1u8, 2, 3]);
+    }
+
+    #[test]
+    fn test_rustbuffer_empty() {
+        let rbuf = RustBuffer::new();
+        assert_eq!(rbuf.len(), 0);
+        // Rust will never give us a null pointer, even for an empty buffer.
+        assert!(!rbuf.data.is_null());
+        assert_eq!(rbuf.destroy_into_vec(), Vec::<u8>::new());
+    }
+
+    #[test]
+    fn test_rustbuffer_new_with_size() {
+        let rbuf = RustBuffer::new_with_size(5);
+        assert_eq!(rbuf.destroy_into_vec().as_slice(), &[0u8, 0, 0, 0, 0]);
+
+        let rbuf = RustBuffer::new_with_size(0);
+        assert!(!rbuf.data.is_null());
+        assert_eq!(rbuf.destroy_into_vec().as_slice(), &[0u8; 0]);
+    }
+
+    #[test]
+    fn test_rustbuffer_null_means_empty() {
+        // This is how foreign-language code might cheaply indicate an empty buffer.
+        let rbuf = unsafe { RustBuffer::from_raw_parts(std::ptr::null_mut(), 0, 0) };
+        assert_eq!(rbuf.destroy_into_vec().as_slice(), &[0u8; 0]);
+    }
+
+    #[test]
+    #[should_panic]
+    fn test_rustbuffer_null_must_have_no_capacity() {
+        // We guard against foreign-language code providing this kind of invalid struct.
+        let rbuf = unsafe { RustBuffer::from_raw_parts(std::ptr::null_mut(), 0, 1) };
+        rbuf.destroy_into_vec();
+    }
+    #[test]
+    #[should_panic]
+    fn test_rustbuffer_null_must_have_zero_length() {
+        // We guard against foreign-language code providing this kind of invalid struct.
+        let rbuf = unsafe { RustBuffer::from_raw_parts(std::ptr::null_mut(), 12, 0) };
+        rbuf.destroy_into_vec();
+    }
+
+    #[test]
+    #[should_panic]
+    fn test_rustbuffer_provided_capacity_must_be_non_negative() {
+        // We guard against foreign-language code providing this kind of invalid struct.
+        let mut v = vec![0u8, 1, 2];
+        let rbuf = unsafe { RustBuffer::from_raw_parts(v.as_mut_ptr(), 3, -7) };
+        rbuf.destroy_into_vec();
+    }
+
+    #[test]
+    #[should_panic]
+    fn test_rustbuffer_provided_len_must_be_non_negative() {
+        // We guard against foreign-language code providing this kind of invalid struct.
+        let mut v = vec![0u8, 1, 2];
+        let rbuf = unsafe { RustBuffer::from_raw_parts(v.as_mut_ptr(), -1, 3) };
+        rbuf.destroy_into_vec();
+    }
+
+    #[test]
+    #[should_panic]
+    fn test_rustbuffer_provided_len_must_not_exceed_capacity() {
+        // We guard against foreign-language code providing this kind of invalid struct.
+        let mut v = vec![0u8, 1, 2];
+        let rbuf = unsafe { RustBuffer::from_raw_parts(v.as_mut_ptr(), 3, 2) };
+        rbuf.destroy_into_vec();
+    }
+}
diff --git a/src/ffi/rustcalls.rs b/src/ffi/rustcalls.rs
new file mode 100644
index 0000000..51204ef
--- /dev/null
+++ b/src/ffi/rustcalls.rs
@@ -0,0 +1,245 @@
+/* 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/. */
+
+//! # Low-level support for calling rust functions
+//!
+//! This module helps the scaffolding code make calls to rust functions and pass back the result to the FFI bindings code.
+//!
+//! It handles:
+//!    - Catching panics
+//!    - Adapting the result of `Return::lower_return()` into either a return value or an
+//!      exception
+
+use crate::{FfiDefault, Lower, RustBuffer, UniFfiTag};
+use std::mem::MaybeUninit;
+use std::panic;
+
+/// Represents the success/error of a rust call
+///
+/// ## Usage
+///
+/// - The consumer code creates a [RustCallStatus] with an empty [RustBuffer] and
+///   [RustCallStatusCode::Success] (0) as the status code
+/// - A pointer to this object is passed to the rust FFI function.  This is an
+///   "out parameter" which will be updated with any error that occurred during the function's
+///   execution.
+/// - After the call, if `code` is [RustCallStatusCode::Error] or [RustCallStatusCode::UnexpectedError]
+///   then `error_buf` will be updated to contain a serialized error object.   See
+///   [RustCallStatusCode] for what gets serialized. The consumer is responsible for freeing `error_buf`.
+///
+/// ## Layout/fields
+///
+/// The layout of this struct is important since consumers on the other side of the FFI need to
+/// construct it.  If this were a C struct, it would look like:
+///
+/// ```c,no_run
+/// struct RustCallStatus {
+///     int8_t code;
+///     RustBuffer error_buf;
+/// };
+/// ```
+#[repr(C)]
+pub struct RustCallStatus {
+    pub code: RustCallStatusCode,
+    // code is signed because unsigned types are experimental in Kotlin
+    pub error_buf: MaybeUninit<RustBuffer>,
+    // error_buf is MaybeUninit to avoid dropping the value that the consumer code sends in:
+    //   - Consumers should send in a zeroed out RustBuffer.  In this case dropping is a no-op and
+    //     avoiding the drop is a small optimization.
+    //   - If consumers pass in invalid data, then we should avoid trying to drop it.  In
+    //     particular, we don't want to try to free any data the consumer has allocated.
+    //
+    // `MaybeUninit` requires unsafe code, since we are preventing rust from dropping the value.
+    // To use this safely we need to make sure that no code paths set this twice, since that will
+    // leak the first `RustBuffer`.
+}
+
+impl RustCallStatus {
+    pub fn cancelled() -> Self {
+        Self {
+            code: RustCallStatusCode::Cancelled,
+            error_buf: MaybeUninit::new(RustBuffer::new()),
+        }
+    }
+
+    pub fn error(message: impl Into<String>) -> Self {
+        Self {
+            code: RustCallStatusCode::UnexpectedError,
+            error_buf: MaybeUninit::new(<String as Lower<UniFfiTag>>::lower(message.into())),
+        }
+    }
+}
+
+impl Default for RustCallStatus {
+    fn default() -> Self {
+        Self {
+            code: RustCallStatusCode::Success,
+            error_buf: MaybeUninit::uninit(),
+        }
+    }
+}
+
+/// Result of a FFI call to a Rust function
+#[repr(i8)]
+#[derive(Debug, PartialEq, Eq)]
+pub enum RustCallStatusCode {
+    /// Successful call.
+    Success = 0,
+    /// Expected error, corresponding to the `Result::Err` variant.  [RustCallStatus::error_buf]
+    /// will contain the serialized error.
+    Error = 1,
+    /// Unexpected error.  [RustCallStatus::error_buf] will contain a serialized message string
+    UnexpectedError = 2,
+    /// Async function cancelled.  [RustCallStatus::error_buf] will be empty and does not need to
+    /// be freed.
+    ///
+    /// This is only returned for async functions and only if the bindings code uses the
+    /// [rust_future_cancel] call.
+    Cancelled = 3,
+}
+
+/// Handle a scaffolding calls
+///
+/// `callback` is responsible for making the actual Rust call and returning a special result type:
+///   - For successful calls, return `Ok(value)`
+///   - For errors that should be translated into thrown exceptions in the foreign code, serialize
+///     the error into a `RustBuffer`, then return `Ok(buf)`
+///   - The success type, must implement `FfiDefault`.
+///   - `Return::lower_return` returns `Result<>` types that meet the above criteria>
+/// - If the function returns a `Ok` value it will be unwrapped and returned
+/// - If the function returns a `Err` value:
+///     - `out_status.code` will be set to [RustCallStatusCode::Error].
+///     - `out_status.error_buf` will be set to a newly allocated `RustBuffer` containing the error.  The calling
+///       code is responsible for freeing the `RustBuffer`
+///     - `FfiDefault::ffi_default()` is returned, although foreign code should ignore this value
+/// - If the function panics:
+///     - `out_status.code` will be set to `CALL_PANIC`
+///     - `out_status.error_buf` will be set to a newly allocated `RustBuffer` containing a
+///       serialized error message.  The calling code is responsible for freeing the `RustBuffer`
+///     - `FfiDefault::ffi_default()` is returned, although foreign code should ignore this value
+pub fn rust_call<F, R>(out_status: &mut RustCallStatus, callback: F) -> R
+where
+    F: panic::UnwindSafe + FnOnce() -> Result<R, RustBuffer>,
+    R: FfiDefault,
+{
+    rust_call_with_out_status(out_status, callback).unwrap_or_else(R::ffi_default)
+}
+
+/// Make a Rust call and update `RustCallStatus` based on the result.
+///
+/// If the call succeeds this returns Some(v) and doesn't touch out_status
+/// If the call fails (including Err results), this returns None and updates out_status
+///
+/// This contains the shared code between `rust_call` and `rustfuture::do_wake`.
+pub(crate) fn rust_call_with_out_status<F, R>(
+    out_status: &mut RustCallStatus,
+    callback: F,
+) -> Option<R>
+where
+    F: panic::UnwindSafe + FnOnce() -> Result<R, RustBuffer>,
+{
+    let result = panic::catch_unwind(|| {
+        crate::panichook::ensure_setup();
+        callback()
+    });
+    match result {
+        // Happy path.  Note: no need to update out_status in this case because the calling code
+        // initializes it to [RustCallStatusCode::Success]
+        Ok(Ok(v)) => Some(v),
+        // Callback returned an Err.
+        Ok(Err(buf)) => {
+            out_status.code = RustCallStatusCode::Error;
+            unsafe {
+                // Unsafe because we're setting the `MaybeUninit` value, see above for safety
+                // invariants.
+                out_status.error_buf.as_mut_ptr().write(buf);
+            }
+            None
+        }
+        // Callback panicked
+        Err(cause) => {
+            out_status.code = RustCallStatusCode::UnexpectedError;
+            // Try to coerce the cause into a RustBuffer containing a String.  Since this code can
+            // panic, we need to use a second catch_unwind().
+            let message_result = panic::catch_unwind(panic::AssertUnwindSafe(move || {
+                // The documentation suggests that it will *usually* be a str or String.
+                let message = if let Some(s) = cause.downcast_ref::<&'static str>() {
+                    (*s).to_string()
+                } else if let Some(s) = cause.downcast_ref::<String>() {
+                    s.clone()
+                } else {
+                    "Unknown panic!".to_string()
+                };
+                log::error!("Caught a panic calling rust code: {:?}", message);
+                <String as Lower<UniFfiTag>>::lower(message)
+            }));
+            if let Ok(buf) = message_result {
+                unsafe {
+                    // Unsafe because we're setting the `MaybeUninit` value, see above for safety
+                    // invariants.
+                    out_status.error_buf.as_mut_ptr().write(buf);
+                }
+            }
+            // Ignore the error case.  We've done all that we can at this point.  In the bindings
+            // code, we handle this by checking if `error_buf` still has an empty `RustBuffer` and
+            // using a generic message.
+            None
+        }
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+    use crate::{test_util::TestError, Lift, LowerReturn};
+
+    fn create_call_status() -> RustCallStatus {
+        RustCallStatus {
+            code: RustCallStatusCode::Success,
+            error_buf: MaybeUninit::new(RustBuffer::new()),
+        }
+    }
+
+    fn test_callback(a: u8) -> Result<i8, TestError> {
+        match a {
+            0 => Ok(100),
+            1 => Err(TestError("Error".to_owned())),
+            x => panic!("Unexpected value: {x}"),
+        }
+    }
+
+    #[test]
+    fn test_rust_call() {
+        let mut status = create_call_status();
+        let return_value = rust_call(&mut status, || {
+            <Result<i8, TestError> as LowerReturn<UniFfiTag>>::lower_return(test_callback(0))
+        });
+
+        assert_eq!(status.code, RustCallStatusCode::Success);
+        assert_eq!(return_value, 100);
+
+        rust_call(&mut status, || {
+            <Result<i8, TestError> as LowerReturn<UniFfiTag>>::lower_return(test_callback(1))
+        });
+        assert_eq!(status.code, RustCallStatusCode::Error);
+        unsafe {
+            assert_eq!(
+                <TestError as Lift<UniFfiTag>>::try_lift(status.error_buf.assume_init()).unwrap(),
+                TestError("Error".to_owned())
+            );
+        }
+
+        let mut status = create_call_status();
+        rust_call(&mut status, || {
+            <Result<i8, TestError> as LowerReturn<UniFfiTag>>::lower_return(test_callback(2))
+        });
+        assert_eq!(status.code, RustCallStatusCode::UnexpectedError);
+        unsafe {
+            assert_eq!(
+                <String as Lift<UniFfiTag>>::try_lift(status.error_buf.assume_init()).unwrap(),
+                "Unexpected value: 2"
+            );
+        }
+    }
+}
diff --git a/src/ffi/rustfuture/future.rs b/src/ffi/rustfuture/future.rs
new file mode 100644
index 0000000..b104b20
--- /dev/null
+++ b/src/ffi/rustfuture/future.rs
@@ -0,0 +1,320 @@
+/* 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/. */
+
+//! [`RustFuture`] represents a [`Future`] that can be sent to the foreign code over FFI.
+//!
+//! This type is not instantiated directly, but via the procedural macros, such as `#[uniffi::export]`.
+//!
+//! # The big picture
+//!
+//! We implement async foreign functions using a simplified version of the Future API:
+//!
+//! 0. At startup, register a [RustFutureContinuationCallback] by calling
+//!    rust_future_continuation_callback_set.
+//! 1. Call the scaffolding function to get a [RustFutureHandle]
+//! 2a. In a loop:
+//!   - Call [rust_future_poll]
+//!   - Suspend the function until the [rust_future_poll] continuation function is called
+//!   - If the continuation was function was called with [RustFuturePoll::Ready], then break
+//!     otherwise continue.
+//! 2b. If the async function is cancelled, then call [rust_future_cancel].  This causes the
+//!     continuation function to be called with [RustFuturePoll::Ready] and the [RustFuture] to
+//!     enter a cancelled state.
+//! 3. Call [rust_future_complete] to get the result of the future.
+//! 4. Call [rust_future_free] to free the future, ideally in a finally block.  This:
+//!    - Releases any resources held by the future
+//!    - Calls any continuation callbacks that have not been called yet
+//!
+//! Note: Technically, the foreign code calls the scaffolding versions of the `rust_future_*`
+//! functions.  These are generated by the scaffolding macro, specially prefixed, and extern "C",
+//! and manually monomorphized in the case of [rust_future_complete].  See
+//! `uniffi_macros/src/setup_scaffolding.rs` for details.
+//!
+//! ## How does `Future` work exactly?
+//!
+//! A [`Future`] in Rust does nothing. When calling an async function, it just
+//! returns a `Future` but nothing has happened yet. To start the computation,
+//! the future must be polled. It returns [`Poll::Ready(r)`][`Poll::Ready`] if
+//! the result is ready, [`Poll::Pending`] otherwise. `Poll::Pending` basically
+//! means:
+//!
+//! > Please, try to poll me later, maybe the result will be ready!
+//!
+//! This model is very different than what other languages do, but it can actually
+//! be translated quite easily, fortunately for us!
+//!
+//! But… wait a minute… who is responsible to poll the `Future` if a `Future` does
+//! nothing? Well, it's _the executor_. The executor is responsible _to drive_ the
+//! `Future`: that's where they are polled.
+//!
+//! But… wait another minute… how does the executor know when to poll a [`Future`]?
+//! Does it poll them randomly in an endless loop? Well, no, actually it depends
+//! on the executor! A well-designed `Future` and executor work as follows.
+//! Normally, when [`Future::poll`] is called, a [`Context`] argument is
+//! passed to it. It contains a [`Waker`]. The [`Waker`] is built on top of a
+//! [`RawWaker`] which implements whatever is necessary. Usually, a waker will
+//! signal the executor to poll a particular `Future`. A `Future` will clone
+//! or pass-by-ref the waker to somewhere, as a callback, a completion, a
+//! function, or anything, to the system that is responsible to notify when a
+//! task is completed. So, to recap, the waker is _not_ responsible for waking the
+//! `Future`, it _is_ responsible for _signaling_ the executor that a particular
+//! `Future` should be polled again. That's why the documentation of
+//! [`Poll::Pending`] specifies:
+//!
+//! > When a function returns `Pending`, the function must also ensure that the
+//! > current task is scheduled to be awoken when progress can be made.
+//!
+//! “awakening” is done by using the `Waker`.
+//!
+//! [`Future`]: https://doc.rust-lang.org/std/future/trait.Future.html
+//! [`Future::poll`]: https://doc.rust-lang.org/std/future/trait.Future.html#tymethod.poll
+//! [`Pol::Ready`]: https://doc.rust-lang.org/std/task/enum.Poll.html#variant.Ready
+//! [`Poll::Pending`]: https://doc.rust-lang.org/std/task/enum.Poll.html#variant.Pending
+//! [`Context`]: https://doc.rust-lang.org/std/task/struct.Context.html
+//! [`Waker`]: https://doc.rust-lang.org/std/task/struct.Waker.html
+//! [`RawWaker`]: https://doc.rust-lang.org/std/task/struct.RawWaker.html
+
+use std::{
+    future::Future,
+    marker::PhantomData,
+    ops::Deref,
+    panic,
+    pin::Pin,
+    sync::{Arc, Mutex},
+    task::{Context, Poll, Wake},
+};
+
+use super::{RustFutureContinuationCallback, RustFuturePoll, Scheduler};
+use crate::{rust_call_with_out_status, FfiDefault, LowerReturn, RustCallStatus};
+
+/// Wraps the actual future we're polling
+struct WrappedFuture<F, T, UT>
+where
+    // See rust_future_new for an explanation of these trait bounds
+    F: Future<Output = T> + Send + 'static,
+    T: LowerReturn<UT> + Send + 'static,
+    UT: Send + 'static,
+{
+    // Note: this could be a single enum, but that would make it easy to mess up the future pinning
+    // guarantee.   For example you might want to call `std::mem::take()` to try to get the result,
+    // but if the future happened to be stored that would move and break all internal references.
+    future: Option<F>,
+    result: Option<Result<T::ReturnType, RustCallStatus>>,
+}
+
+impl<F, T, UT> WrappedFuture<F, T, UT>
+where
+    // See rust_future_new for an explanation of these trait bounds
+    F: Future<Output = T> + Send + 'static,
+    T: LowerReturn<UT> + Send + 'static,
+    UT: Send + 'static,
+{
+    fn new(future: F) -> Self {
+        Self {
+            future: Some(future),
+            result: None,
+        }
+    }
+
+    // Poll the future and check if it's ready or not
+    fn poll(&mut self, context: &mut Context<'_>) -> bool {
+        if self.result.is_some() {
+            true
+        } else if let Some(future) = &mut self.future {
+            // SAFETY: We can call Pin::new_unchecked because:
+            //    - This is the only time we get a &mut to `self.future`
+            //    - We never poll the future after it's moved (for example by using take())
+            //    - We never move RustFuture, which contains us.
+            //    - RustFuture is private to this module so no other code can move it.
+            let pinned = unsafe { Pin::new_unchecked(future) };
+            // Run the poll and lift the result if it's ready
+            let mut out_status = RustCallStatus::default();
+            let result: Option<Poll<T::ReturnType>> = rust_call_with_out_status(
+                &mut out_status,
+                // This closure uses a `&mut F` value, which means it's not UnwindSafe by
+                // default.  If the future panics, it may be in an invalid state.
+                //
+                // However, we can safely use `AssertUnwindSafe` since a panic will lead the `None`
+                // case below and we will never poll the future again.
+                panic::AssertUnwindSafe(|| match pinned.poll(context) {
+                    Poll::Pending => Ok(Poll::Pending),
+                    Poll::Ready(v) => T::lower_return(v).map(Poll::Ready),
+                }),
+            );
+            match result {
+                Some(Poll::Pending) => false,
+                Some(Poll::Ready(v)) => {
+                    self.future = None;
+                    self.result = Some(Ok(v));
+                    true
+                }
+                None => {
+                    self.future = None;
+                    self.result = Some(Err(out_status));
+                    true
+                }
+            }
+        } else {
+            log::error!("poll with neither future nor result set");
+            true
+        }
+    }
+
+    fn complete(&mut self, out_status: &mut RustCallStatus) -> T::ReturnType {
+        let mut return_value = T::ReturnType::ffi_default();
+        match self.result.take() {
+            Some(Ok(v)) => return_value = v,
+            Some(Err(call_status)) => *out_status = call_status,
+            None => *out_status = RustCallStatus::cancelled(),
+        }
+        self.free();
+        return_value
+    }
+
+    fn free(&mut self) {
+        self.future = None;
+        self.result = None;
+    }
+}
+
+// If F and T are Send, then WrappedFuture is too
+//
+// Rust will not mark it Send by default when T::ReturnType is a raw pointer.  This is promising
+// that we will treat the raw pointer properly, for example by not returning it twice.
+unsafe impl<F, T, UT> Send for WrappedFuture<F, T, UT>
+where
+    // See rust_future_new for an explanation of these trait bounds
+    F: Future<Output = T> + Send + 'static,
+    T: LowerReturn<UT> + Send + 'static,
+    UT: Send + 'static,
+{
+}
+
+/// Future that the foreign code is awaiting
+pub(super) struct RustFuture<F, T, UT>
+where
+    // See rust_future_new for an explanation of these trait bounds
+    F: Future<Output = T> + Send + 'static,
+    T: LowerReturn<UT> + Send + 'static,
+    UT: Send + 'static,
+{
+    // This Mutex should never block if our code is working correctly, since there should not be
+    // multiple threads calling [Self::poll] and/or [Self::complete] at the same time.
+    future: Mutex<WrappedFuture<F, T, UT>>,
+    scheduler: Mutex<Scheduler>,
+    // UT is used as the generic parameter for [LowerReturn].
+    // Let's model this with PhantomData as a function that inputs a UT value.
+    _phantom: PhantomData<fn(UT) -> ()>,
+}
+
+impl<F, T, UT> RustFuture<F, T, UT>
+where
+    // See rust_future_new for an explanation of these trait bounds
+    F: Future<Output = T> + Send + 'static,
+    T: LowerReturn<UT> + Send + 'static,
+    UT: Send + 'static,
+{
+    pub(super) fn new(future: F, _tag: UT) -> Arc<Self> {
+        Arc::new(Self {
+            future: Mutex::new(WrappedFuture::new(future)),
+            scheduler: Mutex::new(Scheduler::new()),
+            _phantom: PhantomData,
+        })
+    }
+
+    pub(super) fn poll(self: Arc<Self>, callback: RustFutureContinuationCallback, data: *const ()) {
+        let ready = self.is_cancelled() || {
+            let mut locked = self.future.lock().unwrap();
+            let waker: std::task::Waker = Arc::clone(&self).into();
+            locked.poll(&mut Context::from_waker(&waker))
+        };
+        if ready {
+            callback(data, RustFuturePoll::Ready)
+        } else {
+            self.scheduler.lock().unwrap().store(callback, data);
+        }
+    }
+
+    pub(super) fn is_cancelled(&self) -> bool {
+        self.scheduler.lock().unwrap().is_cancelled()
+    }
+
+    pub(super) fn wake(&self) {
+        self.scheduler.lock().unwrap().wake();
+    }
+
+    pub(super) fn cancel(&self) {
+        self.scheduler.lock().unwrap().cancel();
+    }
+
+    pub(super) fn complete(&self, call_status: &mut RustCallStatus) -> T::ReturnType {
+        self.future.lock().unwrap().complete(call_status)
+    }
+
+    pub(super) fn free(self: Arc<Self>) {
+        // Call cancel() to send any leftover data to the continuation callback
+        self.scheduler.lock().unwrap().cancel();
+        // Ensure we drop our inner future, releasing all held references
+        self.future.lock().unwrap().free();
+    }
+}
+
+impl<F, T, UT> Wake for RustFuture<F, T, UT>
+where
+    // See rust_future_new for an explanation of these trait bounds
+    F: Future<Output = T> + Send + 'static,
+    T: LowerReturn<UT> + Send + 'static,
+    UT: Send + 'static,
+{
+    fn wake(self: Arc<Self>) {
+        self.deref().wake()
+    }
+
+    fn wake_by_ref(self: &Arc<Self>) {
+        self.deref().wake()
+    }
+}
+
+/// RustFuture FFI trait.  This allows `Arc<RustFuture<F, T, UT>>` to be cast to
+/// `Arc<dyn RustFutureFfi<T::ReturnType>>`, which is needed to implement the public FFI API.  In particular, this
+/// allows you to use RustFuture functionality without knowing the concrete Future type, which is
+/// unnamable.
+///
+/// This is parametrized on the ReturnType rather than the `T` directly, to reduce the number of
+/// scaffolding functions we need to generate.  If it was parametrized on `T`, then we would need
+/// to create a poll, cancel, complete, and free scaffolding function for each exported async
+/// function.  That would add ~1kb binary size per exported function based on a quick estimate on a
+/// x86-64 machine . By parametrizing on `T::ReturnType` we can instead monomorphize by hand and
+/// only create those functions for each of the 13 possible FFI return types.
+#[doc(hidden)]
+pub trait RustFutureFfi<ReturnType> {
+    fn ffi_poll(self: Arc<Self>, callback: RustFutureContinuationCallback, data: *const ());
+    fn ffi_cancel(&self);
+    fn ffi_complete(&self, call_status: &mut RustCallStatus) -> ReturnType;
+    fn ffi_free(self: Arc<Self>);
+}
+
+impl<F, T, UT> RustFutureFfi<T::ReturnType> for RustFuture<F, T, UT>
+where
+    // See rust_future_new for an explanation of these trait bounds
+    F: Future<Output = T> + Send + 'static,
+    T: LowerReturn<UT> + Send + 'static,
+    UT: Send + 'static,
+{
+    fn ffi_poll(self: Arc<Self>, callback: RustFutureContinuationCallback, data: *const ()) {
+        self.poll(callback, data)
+    }
+
+    fn ffi_cancel(&self) {
+        self.cancel()
+    }
+
+    fn ffi_complete(&self, call_status: &mut RustCallStatus) -> T::ReturnType {
+        self.complete(call_status)
+    }
+
+    fn ffi_free(self: Arc<Self>) {
+        self.free();
+    }
+}
diff --git a/src/ffi/rustfuture/mod.rs b/src/ffi/rustfuture/mod.rs
new file mode 100644
index 0000000..4aaf013
--- /dev/null
+++ b/src/ffi/rustfuture/mod.rs
@@ -0,0 +1,126 @@
+/* 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 std::{future::Future, sync::Arc};
+
+mod future;
+mod scheduler;
+use future::*;
+use scheduler::*;
+
+#[cfg(test)]
+mod tests;
+
+use crate::{LowerReturn, RustCallStatus};
+
+/// Result code for [rust_future_poll].  This is passed to the continuation function.
+#[repr(i8)]
+#[derive(Debug, PartialEq, Eq)]
+pub enum RustFuturePoll {
+    /// The future is ready and is waiting for [rust_future_complete] to be called
+    Ready = 0,
+    /// The future might be ready and [rust_future_poll] should be called again
+    MaybeReady = 1,
+}
+
+/// Foreign callback that's passed to [rust_future_poll]
+///
+/// The Rust side of things calls this when the foreign side should call [rust_future_poll] again
+/// to continue progress on the future.
+pub type RustFutureContinuationCallback = extern "C" fn(callback_data: *const (), RustFuturePoll);
+
+/// Opaque handle for a Rust future that's stored by the foreign language code
+#[repr(transparent)]
+pub struct RustFutureHandle(*const ());
+
+// === Public FFI API ===
+
+/// Create a new [RustFutureHandle]
+///
+/// For each exported async function, UniFFI will create a scaffolding function that uses this to
+/// create the [RustFutureHandle] to pass to the foreign code.
+pub fn rust_future_new<F, T, UT>(future: F, tag: UT) -> RustFutureHandle
+where
+    // F is the future type returned by the exported async function.  It needs to be Send + `static
+    // since it will move between threads for an indeterminate amount of time as the foreign
+    // executor calls polls it and the Rust executor wakes it.  It does not need to by `Sync`,
+    // since we synchronize all access to the values.
+    F: Future<Output = T> + Send + 'static,
+    // T is the output of the Future.  It needs to implement [LowerReturn].  Also it must be Send +
+    // 'static for the same reason as F.
+    T: LowerReturn<UT> + Send + 'static,
+    // The UniFfiTag ZST. The Send + 'static bound is to keep rustc happy.
+    UT: Send + 'static,
+{
+    // Create a RustFuture and coerce to `Arc<dyn RustFutureFfi>`, which is what we use to
+    // implement the FFI
+    let future_ffi = RustFuture::new(future, tag) as Arc<dyn RustFutureFfi<T::ReturnType>>;
+    // Box the Arc, to convert the wide pointer into a normal sized pointer so that we can pass it
+    // to the foreign code.
+    let boxed_ffi = Box::new(future_ffi);
+    // We can now create a RustFutureHandle
+    RustFutureHandle(Box::into_raw(boxed_ffi) as *mut ())
+}
+
+/// Poll a Rust future
+///
+/// When the future is ready to progress the continuation will be called with the `data` value and
+/// a [RustFuturePoll] value. For each [rust_future_poll] call the continuation will be called
+/// exactly once.
+///
+/// # Safety
+///
+/// The [RustFutureHandle] must not previously have been passed to [rust_future_free]
+pub unsafe fn rust_future_poll<ReturnType>(
+    handle: RustFutureHandle,
+    callback: RustFutureContinuationCallback,
+    data: *const (),
+) {
+    let future = &*(handle.0 as *mut Arc<dyn RustFutureFfi<ReturnType>>);
+    future.clone().ffi_poll(callback, data)
+}
+
+/// Cancel a Rust future
+///
+/// Any current and future continuations will be immediately called with RustFuturePoll::Ready.
+///
+/// This is needed for languages like Swift, which continuation to wait for the continuation to be
+/// called when tasks are cancelled.
+///
+/// # Safety
+///
+/// The [RustFutureHandle] must not previously have been passed to [rust_future_free]
+pub unsafe fn rust_future_cancel<ReturnType>(handle: RustFutureHandle) {
+    let future = &*(handle.0 as *mut Arc<dyn RustFutureFfi<ReturnType>>);
+    future.clone().ffi_cancel()
+}
+
+/// Complete a Rust future
+///
+/// Note: the actually extern "C" scaffolding functions can't be generic, so we generate one for
+/// each supported FFI type.
+///
+/// # Safety
+///
+/// - The [RustFutureHandle] must not previously have been passed to [rust_future_free]
+/// - The `T` param must correctly correspond to the [rust_future_new] call.  It must
+///   be `<Output as LowerReturn<UT>>::ReturnType`
+pub unsafe fn rust_future_complete<ReturnType>(
+    handle: RustFutureHandle,
+    out_status: &mut RustCallStatus,
+) -> ReturnType {
+    let future = &*(handle.0 as *mut Arc<dyn RustFutureFfi<ReturnType>>);
+    future.ffi_complete(out_status)
+}
+
+/// Free a Rust future, dropping the strong reference and releasing all references held by the
+/// future.
+///
+/// # Safety
+///
+/// The [RustFutureHandle] must not previously have been passed to [rust_future_free]
+pub unsafe fn rust_future_free<ReturnType>(handle: RustFutureHandle) {
+    let future = Box::from_raw(handle.0 as *mut Arc<dyn RustFutureFfi<ReturnType>>);
+    future.ffi_free()
+}
diff --git a/src/ffi/rustfuture/scheduler.rs b/src/ffi/rustfuture/scheduler.rs
new file mode 100644
index 0000000..aae5a0c
--- /dev/null
+++ b/src/ffi/rustfuture/scheduler.rs
@@ -0,0 +1,96 @@
+/* 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 std::mem;
+
+use super::{RustFutureContinuationCallback, RustFuturePoll};
+
+/// Schedules a [crate::RustFuture] by managing the continuation data
+///
+/// This struct manages the continuation callback and data that comes from the foreign side.  It
+/// is responsible for calling the continuation callback when the future is ready to be woken up.
+///
+/// The basic guarantees are:
+///
+/// * Each callback will be invoked exactly once, with its associated data.
+/// * If `wake()` is called, the callback will be invoked to wake up the future -- either
+///   immediately or the next time we get a callback.
+/// * If `cancel()` is called, the same will happen and the schedule will stay in the cancelled
+///   state, invoking any future callbacks as soon as they're stored.
+
+#[derive(Debug)]
+pub(super) enum Scheduler {
+    /// No continuations set, neither wake() nor cancel() called.
+    Empty,
+    /// `wake()` was called when there was no continuation set.  The next time `store` is called,
+    /// the continuation should be immediately invoked with `RustFuturePoll::MaybeReady`
+    Waked,
+    /// The future has been cancelled, any future `store` calls should immediately result in the
+    /// continuation being called with `RustFuturePoll::Ready`.
+    Cancelled,
+    /// Continuation set, the next time `wake()`  is called is called, we should invoke it.
+    Set(RustFutureContinuationCallback, *const ()),
+}
+
+impl Scheduler {
+    pub(super) fn new() -> Self {
+        Self::Empty
+    }
+
+    /// Store new continuation data if we are in the `Empty` state.  If we are in the `Waked` or
+    /// `Cancelled` state, call the continuation immediately with the data.
+    pub(super) fn store(&mut self, callback: RustFutureContinuationCallback, data: *const ()) {
+        match self {
+            Self::Empty => *self = Self::Set(callback, data),
+            Self::Set(old_callback, old_data) => {
+                log::error!(
+                    "store: observed `Self::Set` state.  Is poll() being called from multiple threads at once?"
+                );
+                old_callback(*old_data, RustFuturePoll::Ready);
+                *self = Self::Set(callback, data);
+            }
+            Self::Waked => {
+                *self = Self::Empty;
+                callback(data, RustFuturePoll::MaybeReady);
+            }
+            Self::Cancelled => {
+                callback(data, RustFuturePoll::Ready);
+            }
+        }
+    }
+
+    pub(super) fn wake(&mut self) {
+        match self {
+            // If we had a continuation set, then call it and transition to the `Empty` state.
+            Self::Set(callback, old_data) => {
+                let old_data = *old_data;
+                let callback = *callback;
+                *self = Self::Empty;
+                callback(old_data, RustFuturePoll::MaybeReady);
+            }
+            // If we were in the `Empty` state, then transition to `Waked`.  The next time `store`
+            // is called, we will immediately call the continuation.
+            Self::Empty => *self = Self::Waked,
+            // This is a no-op if we were in the `Cancelled` or `Waked` state.
+            _ => (),
+        }
+    }
+
+    pub(super) fn cancel(&mut self) {
+        if let Self::Set(callback, old_data) = mem::replace(self, Self::Cancelled) {
+            callback(old_data, RustFuturePoll::Ready);
+        }
+    }
+
+    pub(super) fn is_cancelled(&self) -> bool {
+        matches!(self, Self::Cancelled)
+    }
+}
+
+// The `*const ()` data pointer references an object on the foreign side.
+// This object must be `Sync` in Rust terminology -- it must be safe for us to pass the pointer to the continuation callback from any thread.
+// If the foreign side upholds their side of the contract, then `Scheduler` is Send + Sync.
+
+unsafe impl Send for Scheduler {}
+unsafe impl Sync for Scheduler {}
diff --git a/src/ffi/rustfuture/tests.rs b/src/ffi/rustfuture/tests.rs
new file mode 100644
index 0000000..1f68085
--- /dev/null
+++ b/src/ffi/rustfuture/tests.rs
@@ -0,0 +1,223 @@
+use once_cell::sync::OnceCell;
+use std::{
+    future::Future,
+    panic,
+    pin::Pin,
+    sync::{Arc, Mutex},
+    task::{Context, Poll, Waker},
+};
+
+use super::*;
+use crate::{test_util::TestError, Lift, RustBuffer, RustCallStatusCode};
+
+// Sender/Receiver pair that we use for testing
+struct Channel {
+    result: Option<Result<String, TestError>>,
+    waker: Option<Waker>,
+}
+
+struct Sender(Arc<Mutex<Channel>>);
+
+impl Sender {
+    fn wake(&self) {
+        let inner = self.0.lock().unwrap();
+        if let Some(waker) = &inner.waker {
+            waker.wake_by_ref();
+        }
+    }
+
+    fn send(&self, value: Result<String, TestError>) {
+        let mut inner = self.0.lock().unwrap();
+        if inner.result.replace(value).is_some() {
+            panic!("value already sent");
+        }
+        if let Some(waker) = &inner.waker {
+            waker.wake_by_ref();
+        }
+    }
+}
+
+struct Receiver(Arc<Mutex<Channel>>);
+
+impl Future for Receiver {
+    type Output = Result<String, TestError>;
+
+    fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Result<String, TestError>> {
+        let mut inner = self.0.lock().unwrap();
+        match &inner.result {
+            Some(v) => Poll::Ready(v.clone()),
+            None => {
+                inner.waker = Some(context.waker().clone());
+                Poll::Pending
+            }
+        }
+    }
+}
+
+// Create a sender and rust future that we can use for testing
+fn channel() -> (Sender, Arc<dyn RustFutureFfi<RustBuffer>>) {
+    let channel = Arc::new(Mutex::new(Channel {
+        result: None,
+        waker: None,
+    }));
+    let rust_future = RustFuture::new(Receiver(channel.clone()), crate::UniFfiTag);
+    (Sender(channel), rust_future)
+}
+
+/// Poll a Rust future and get an OnceCell that's set when the continuation is called
+fn poll(rust_future: &Arc<dyn RustFutureFfi<RustBuffer>>) -> Arc<OnceCell<RustFuturePoll>> {
+    let cell = Arc::new(OnceCell::new());
+    let cell_ptr = Arc::into_raw(cell.clone()) as *const ();
+    rust_future.clone().ffi_poll(poll_continuation, cell_ptr);
+    cell
+}
+
+extern "C" fn poll_continuation(data: *const (), code: RustFuturePoll) {
+    let cell = unsafe { Arc::from_raw(data as *const OnceCell<RustFuturePoll>) };
+    cell.set(code).expect("Error setting OnceCell");
+}
+
+fn complete(rust_future: Arc<dyn RustFutureFfi<RustBuffer>>) -> (RustBuffer, RustCallStatus) {
+    let mut out_status_code = RustCallStatus::default();
+    let return_value = rust_future.ffi_complete(&mut out_status_code);
+    (return_value, out_status_code)
+}
+
+#[test]
+fn test_success() {
+    let (sender, rust_future) = channel();
+
+    // Test polling the rust future before it's ready
+    let continuation_result = poll(&rust_future);
+    assert_eq!(continuation_result.get(), None);
+    sender.wake();
+    assert_eq!(continuation_result.get(), Some(&RustFuturePoll::MaybeReady));
+
+    // Test polling the rust future when it's ready
+    let continuation_result = poll(&rust_future);
+    assert_eq!(continuation_result.get(), None);
+    sender.send(Ok("All done".into()));
+    assert_eq!(continuation_result.get(), Some(&RustFuturePoll::MaybeReady));
+
+    // Future polls should immediately return ready
+    let continuation_result = poll(&rust_future);
+    assert_eq!(continuation_result.get(), Some(&RustFuturePoll::Ready));
+
+    // Complete the future
+    let (return_buf, call_status) = complete(rust_future);
+    assert_eq!(call_status.code, RustCallStatusCode::Success);
+    assert_eq!(
+        <String as Lift<crate::UniFfiTag>>::try_lift(return_buf).unwrap(),
+        "All done"
+    );
+}
+
+#[test]
+fn test_error() {
+    let (sender, rust_future) = channel();
+
+    let continuation_result = poll(&rust_future);
+    assert_eq!(continuation_result.get(), None);
+    sender.send(Err("Something went wrong".into()));
+    assert_eq!(continuation_result.get(), Some(&RustFuturePoll::MaybeReady));
+
+    let continuation_result = poll(&rust_future);
+    assert_eq!(continuation_result.get(), Some(&RustFuturePoll::Ready));
+
+    let (_, call_status) = complete(rust_future);
+    assert_eq!(call_status.code, RustCallStatusCode::Error);
+    unsafe {
+        assert_eq!(
+            <TestError as Lift<crate::UniFfiTag>>::try_lift_from_rust_buffer(
+                call_status.error_buf.assume_init()
+            )
+            .unwrap(),
+            TestError::from("Something went wrong"),
+        )
+    }
+}
+
+// Once `complete` is called, the inner future should be released, even if wakers still hold a
+// reference to the RustFuture
+#[test]
+fn test_cancel() {
+    let (_sender, rust_future) = channel();
+
+    let continuation_result = poll(&rust_future);
+    assert_eq!(continuation_result.get(), None);
+    rust_future.ffi_cancel();
+    // Cancellation should immediately invoke the callback with RustFuturePoll::Ready
+    assert_eq!(continuation_result.get(), Some(&RustFuturePoll::Ready));
+
+    // Future polls should immediately invoke the callback with RustFuturePoll::Ready
+    let continuation_result = poll(&rust_future);
+    assert_eq!(continuation_result.get(), Some(&RustFuturePoll::Ready));
+
+    let (_, call_status) = complete(rust_future);
+    assert_eq!(call_status.code, RustCallStatusCode::Cancelled);
+}
+
+// Once `free` is called, the inner future should be released, even if wakers still hold a
+// reference to the RustFuture
+#[test]
+fn test_release_future() {
+    let (sender, rust_future) = channel();
+    // Create a weak reference to the channel to use to check if rust_future has dropped its
+    // future.
+    let channel_weak = Arc::downgrade(&sender.0);
+    drop(sender);
+    // Create an extra ref to rust_future, simulating a waker that still holds a reference to
+    // it
+    let rust_future2 = rust_future.clone();
+
+    // Complete the rust future
+    rust_future.ffi_free();
+    // Even though rust_future is still alive, the channel shouldn't be
+    assert!(Arc::strong_count(&rust_future2) > 0);
+    assert_eq!(channel_weak.strong_count(), 0);
+    assert!(channel_weak.upgrade().is_none());
+}
+
+// If `free` is called with a continuation still stored, we should call it them then.
+//
+// This shouldn't happen in practice, but it seems like good defensive programming
+#[test]
+fn test_complete_with_stored_continuation() {
+    let (_sender, rust_future) = channel();
+
+    let continuation_result = poll(&rust_future);
+    rust_future.ffi_free();
+    assert_eq!(continuation_result.get(), Some(&RustFuturePoll::Ready));
+}
+
+// Test what happens if we see a `wake()` call while we're polling the future.  This can
+// happen, for example, with futures that are handled by a tokio thread pool.  We should
+// schedule another poll of the future in this case.
+#[test]
+fn test_wake_during_poll() {
+    let mut first_time = true;
+    let future = std::future::poll_fn(move |ctx| {
+        if first_time {
+            first_time = false;
+            // Wake the future while we are in the middle of polling it
+            ctx.waker().clone().wake();
+            Poll::Pending
+        } else {
+            // The second time we're polled, we're ready
+            Poll::Ready("All done".to_owned())
+        }
+    });
+    let rust_future: Arc<dyn RustFutureFfi<RustBuffer>> = RustFuture::new(future, crate::UniFfiTag);
+    let continuation_result = poll(&rust_future);
+    // The continuation function should called immediately
+    assert_eq!(continuation_result.get(), Some(&RustFuturePoll::MaybeReady));
+    // A second poll should finish the future
+    let continuation_result = poll(&rust_future);
+    assert_eq!(continuation_result.get(), Some(&RustFuturePoll::Ready));
+    let (return_buf, call_status) = complete(rust_future);
+    assert_eq!(call_status.code, RustCallStatusCode::Success);
+    assert_eq!(
+        <String as Lift<crate::UniFfiTag>>::try_lift(return_buf).unwrap(),
+        "All done"
+    );
+}
diff --git a/src/ffi_converter_impls.rs b/src/ffi_converter_impls.rs
new file mode 100644
index 0000000..5be5f04
--- /dev/null
+++ b/src/ffi_converter_impls.rs
@@ -0,0 +1,531 @@
+/* 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/. */
+
+/// This module contains builtin `FFIConverter` implementations.  These cover:
+///   - Simple privitive types: u8, i32, String, Arc<T>, etc
+///   - Composite types: Vec<T>, Option<T>, etc.
+///   - SystemTime and Duration, which maybe shouldn`t be built-in, but have been historically and
+///     we want to continue to support them for now.
+///
+/// As described in
+/// https://mozilla.github.io/uniffi-rs/internals/lifting_and_lowering.html#code-generation-and-the-fficonverter-trait,
+/// we use the following system:
+///
+///   - Each UniFFIed crate defines a unit struct named `UniFfiTag`
+///   - We define an `impl FFIConverter<UniFfiTag> for Type` for each type that we want to pass
+///     across the FFI.
+///   - When generating the code, we use the `<T as ::uniffi::FFIConverter<crate::UniFfiTag>>` impl
+///     to lift/lower/serialize types for a crate.
+///
+/// This crate needs to implement `FFIConverter<UT>` on `UniFfiTag` instances for all UniFFI
+/// consumer crates.  To do this, it defines blanket impls like `impl<UT> FFIConverter<UT> for u8`.
+/// "UT" means an arbitrary `UniFfiTag` type.
+use crate::{
+    check_remaining, derive_ffi_traits, ffi_converter_rust_buffer_lift_and_lower, metadata,
+    ConvertError, FfiConverter, Lift, LiftRef, LiftReturn, Lower, LowerReturn, MetadataBuffer,
+    Result, RustBuffer, UnexpectedUniFFICallbackError,
+};
+use anyhow::bail;
+use bytes::buf::{Buf, BufMut};
+use paste::paste;
+use std::{
+    collections::HashMap,
+    convert::TryFrom,
+    error::Error,
+    sync::Arc,
+    time::{Duration, SystemTime},
+};
+
+/// Blanket implementation of `FfiConverter` for numeric primitives.
+///
+/// Numeric primitives have a straightforward mapping into C-compatible numeric types,
+/// sice they are themselves a C-compatible numeric type!
+macro_rules! impl_ffi_converter_for_num_primitive {
+    ($T:ty, $type_code:expr) => {
+        paste! {
+            unsafe impl<UT> FfiConverter<UT> for $T {
+                type FfiType = $T;
+
+                fn lower(obj: $T) -> Self::FfiType {
+                    obj
+                }
+
+                fn try_lift(v: Self::FfiType) -> Result<$T> {
+                    Ok(v)
+                }
+
+                fn write(obj: $T, buf: &mut Vec<u8>) {
+                    buf.[<put_ $T>](obj);
+                }
+
+                fn try_read(buf: &mut &[u8]) -> Result<$T> {
+                    check_remaining(buf, std::mem::size_of::<$T>())?;
+                    Ok(buf.[<get_ $T>]())
+                }
+
+                const TYPE_ID_META: MetadataBuffer = MetadataBuffer::from_code($type_code);
+            }
+        }
+    };
+}
+
+impl_ffi_converter_for_num_primitive!(u8, metadata::codes::TYPE_U8);
+impl_ffi_converter_for_num_primitive!(i8, metadata::codes::TYPE_I8);
+impl_ffi_converter_for_num_primitive!(u16, metadata::codes::TYPE_U16);
+impl_ffi_converter_for_num_primitive!(i16, metadata::codes::TYPE_I16);
+impl_ffi_converter_for_num_primitive!(u32, metadata::codes::TYPE_U32);
+impl_ffi_converter_for_num_primitive!(i32, metadata::codes::TYPE_I32);
+impl_ffi_converter_for_num_primitive!(u64, metadata::codes::TYPE_U64);
+impl_ffi_converter_for_num_primitive!(i64, metadata::codes::TYPE_I64);
+impl_ffi_converter_for_num_primitive!(f32, metadata::codes::TYPE_F32);
+impl_ffi_converter_for_num_primitive!(f64, metadata::codes::TYPE_F64);
+
+/// Support for passing boolean values via the FFI.
+///
+/// Booleans are passed as an `i8` in order to avoid problems with handling
+/// C-compatible boolean values on JVM-based languages.
+unsafe impl<UT> FfiConverter<UT> for bool {
+    type FfiType = i8;
+
+    fn lower(obj: bool) -> Self::FfiType {
+        i8::from(obj)
+    }
+
+    fn try_lift(v: Self::FfiType) -> Result<bool> {
+        Ok(match v {
+            0 => false,
+            1 => true,
+            _ => bail!("unexpected byte for Boolean"),
+        })
+    }
+
+    fn write(obj: bool, buf: &mut Vec<u8>) {
+        buf.put_i8(<Self as FfiConverter<UT>>::lower(obj));
+    }
+
+    fn try_read(buf: &mut &[u8]) -> Result<bool> {
+        check_remaining(buf, 1)?;
+        <Self as FfiConverter<UT>>::try_lift(buf.get_i8())
+    }
+
+    const TYPE_ID_META: MetadataBuffer = MetadataBuffer::from_code(metadata::codes::TYPE_BOOL);
+}
+
+/// Support for passing Strings via the FFI.
+///
+/// Unlike many other implementations of `FfiConverter`, this passes a struct containing
+/// a raw pointer rather than copying the data from one side to the other. This is a
+/// safety hazard, but turns out to be pretty nice for useability. This struct
+/// *must* be a valid `RustBuffer` and it *must* contain valid utf-8 data (in other
+/// words, it *must* be a `Vec<u8>` suitable for use as an actual rust `String`).
+///
+/// When serialized in a buffer, strings are represented as a i32 byte length
+/// followed by utf8-encoded bytes. (It's a signed integer because unsigned types are
+/// currently experimental in Kotlin).
+unsafe impl<UT> FfiConverter<UT> for String {
+    type FfiType = RustBuffer;
+
+    // This returns a struct with a raw pointer to the underlying bytes, so it's very
+    // important that it consume ownership of the String, which is relinquished to the
+    // foreign language code (and can be restored by it passing the pointer back).
+    fn lower(obj: String) -> Self::FfiType {
+        RustBuffer::from_vec(obj.into_bytes())
+    }
+
+    // The argument here *must* be a uniquely-owned `RustBuffer` previously obtained
+    // from `lower` above, and hence must be the bytes of a valid rust string.
+    fn try_lift(v: Self::FfiType) -> Result<String> {
+        let v = v.destroy_into_vec();
+        // This turns the buffer back into a `String` without copying the data
+        // and without re-checking it for validity of the utf8. If the `RustBuffer`
+        // came from a valid String then there's no point in re-checking the utf8,
+        // and if it didn't then bad things are probably going to happen regardless
+        // of whether we check for valid utf8 data or not.
+        Ok(unsafe { String::from_utf8_unchecked(v) })
+    }
+
+    fn write(obj: String, buf: &mut Vec<u8>) {
+        // N.B. `len()` gives us the length in bytes, not in chars or graphemes.
+        // TODO: it would be nice not to panic here.
+        let len = i32::try_from(obj.len()).unwrap();
+        buf.put_i32(len); // We limit strings to u32::MAX bytes
+        buf.put(obj.as_bytes());
+    }
+
+    fn try_read(buf: &mut &[u8]) -> Result<String> {
+        check_remaining(buf, 4)?;
+        let len = usize::try_from(buf.get_i32())?;
+        check_remaining(buf, len)?;
+        // N.B: In the general case `Buf::chunk()` may return partial data.
+        // But in the specific case of `<&[u8] as Buf>` it returns the full slice,
+        // so there is no risk of having less than `len` bytes available here.
+        let bytes = &buf.chunk()[..len];
+        let res = String::from_utf8(bytes.to_vec())?;
+        buf.advance(len);
+        Ok(res)
+    }
+
+    const TYPE_ID_META: MetadataBuffer = MetadataBuffer::from_code(metadata::codes::TYPE_STRING);
+}
+
+/// Support for passing timestamp values via the FFI.
+///
+/// Timestamps values are currently always passed by serializing to a buffer.
+///
+/// Timestamps are represented on the buffer by an i64 that indicates the
+/// direction and the magnitude in seconds of the offset from epoch, and a
+/// u32 that indicates the nanosecond portion of the offset magnitude. The
+/// nanosecond portion is expected to be between 0 and 999,999,999.
+///
+/// To build an epoch offset the absolute value of the seconds portion of the
+/// offset should be combined with the nanosecond portion. This is because
+/// the sign of the seconds portion represents the direction of the offset
+/// overall. The sign of the seconds portion can then be used to determine
+/// if the total offset should be added to or subtracted from the unix epoch.
+unsafe impl<UT> FfiConverter<UT> for SystemTime {
+    ffi_converter_rust_buffer_lift_and_lower!(UT);
+
+    fn write(obj: SystemTime, buf: &mut Vec<u8>) {
+        let mut sign = 1;
+        let epoch_offset = obj
+            .duration_since(SystemTime::UNIX_EPOCH)
+            .unwrap_or_else(|error| {
+                sign = -1;
+                error.duration()
+            });
+        // This panic should never happen as SystemTime typically stores seconds as i64
+        let seconds = sign
+            * i64::try_from(epoch_offset.as_secs())
+                .expect("SystemTime overflow, seconds greater than i64::MAX");
+
+        buf.put_i64(seconds);
+        buf.put_u32(epoch_offset.subsec_nanos());
+    }
+
+    fn try_read(buf: &mut &[u8]) -> Result<SystemTime> {
+        check_remaining(buf, 12)?;
+        let seconds = buf.get_i64();
+        let nanos = buf.get_u32();
+        let epoch_offset = Duration::new(seconds.wrapping_abs() as u64, nanos);
+
+        if seconds >= 0 {
+            Ok(SystemTime::UNIX_EPOCH + epoch_offset)
+        } else {
+            Ok(SystemTime::UNIX_EPOCH - epoch_offset)
+        }
+    }
+
+    const TYPE_ID_META: MetadataBuffer =
+        MetadataBuffer::from_code(metadata::codes::TYPE_SYSTEM_TIME);
+}
+
+/// Support for passing duration values via the FFI.
+///
+/// Duration values are currently always passed by serializing to a buffer.
+///
+/// Durations are represented on the buffer by a u64 that indicates the
+/// magnitude in seconds, and a u32 that indicates the nanosecond portion
+/// of the magnitude. The nanosecond portion is expected to be between 0
+/// and 999,999,999.
+unsafe impl<UT> FfiConverter<UT> for Duration {
+    ffi_converter_rust_buffer_lift_and_lower!(UT);
+
+    fn write(obj: Duration, buf: &mut Vec<u8>) {
+        buf.put_u64(obj.as_secs());
+        buf.put_u32(obj.subsec_nanos());
+    }
+
+    fn try_read(buf: &mut &[u8]) -> Result<Duration> {
+        check_remaining(buf, 12)?;
+        Ok(Duration::new(buf.get_u64(), buf.get_u32()))
+    }
+
+    const TYPE_ID_META: MetadataBuffer = MetadataBuffer::from_code(metadata::codes::TYPE_DURATION);
+}
+
+// Support for passing optional values via the FFI.
+//
+// Optional values are currently always passed by serializing to a buffer.
+// We write either a zero byte for `None`, or a one byte followed by the containing
+// item for `Some`.
+//
+// In future we could do the same optimization as rust uses internally, where the
+// `None` option is represented as a null pointer and the `Some` as a valid pointer,
+// but that seems more fiddly and less safe in the short term, so it can wait.
+
+unsafe impl<UT, T: Lower<UT>> Lower<UT> for Option<T> {
+    type FfiType = RustBuffer;
+
+    fn write(obj: Option<T>, buf: &mut Vec<u8>) {
+        match obj {
+            None => buf.put_i8(0),
+            Some(v) => {
+                buf.put_i8(1);
+                T::write(v, buf);
+            }
+        }
+    }
+
+    fn lower(obj: Option<T>) -> RustBuffer {
+        Self::lower_into_rust_buffer(obj)
+    }
+
+    const TYPE_ID_META: MetadataBuffer =
+        MetadataBuffer::from_code(metadata::codes::TYPE_OPTION).concat(T::TYPE_ID_META);
+}
+
+unsafe impl<UT, T: Lift<UT>> Lift<UT> for Option<T> {
+    type FfiType = RustBuffer;
+
+    fn try_read(buf: &mut &[u8]) -> Result<Option<T>> {
+        check_remaining(buf, 1)?;
+        Ok(match buf.get_i8() {
+            0 => None,
+            1 => Some(T::try_read(buf)?),
+            _ => bail!("unexpected tag byte for Option"),
+        })
+    }
+
+    fn try_lift(buf: RustBuffer) -> Result<Option<T>> {
+        Self::try_lift_from_rust_buffer(buf)
+    }
+
+    const TYPE_ID_META: MetadataBuffer =
+        MetadataBuffer::from_code(metadata::codes::TYPE_OPTION).concat(T::TYPE_ID_META);
+}
+
+// Support for passing vectors of values via the FFI.
+//
+// Vectors are currently always passed by serializing to a buffer.
+// We write a `i32` item count followed by each item in turn.
+// (It's a signed type due to limits of the JVM).
+//
+// Ideally we would pass `Vec<u8>` directly as a `RustBuffer` rather
+// than serializing, and perhaps even pass other vector types using a
+// similar struct. But that's for future work.
+
+unsafe impl<UT, T: Lower<UT>> Lower<UT> for Vec<T> {
+    type FfiType = RustBuffer;
+
+    fn write(obj: Vec<T>, buf: &mut Vec<u8>) {
+        // TODO: would be nice not to panic here :-/
+        let len = i32::try_from(obj.len()).unwrap();
+        buf.put_i32(len); // We limit arrays to i32::MAX items
+        for item in obj {
+            <T as Lower<UT>>::write(item, buf);
+        }
+    }
+
+    fn lower(obj: Vec<T>) -> RustBuffer {
+        Self::lower_into_rust_buffer(obj)
+    }
+
+    const TYPE_ID_META: MetadataBuffer =
+        MetadataBuffer::from_code(metadata::codes::TYPE_VEC).concat(T::TYPE_ID_META);
+}
+
+/// Support for associative arrays via the FFI - `record<u32, u64>` in UDL.
+/// HashMaps are currently always passed by serializing to a buffer.
+/// We write a `i32` entries count followed by each entry (string
+/// key followed by the value) in turn.
+/// (It's a signed type due to limits of the JVM).
+unsafe impl<UT, T: Lift<UT>> Lift<UT> for Vec<T> {
+    type FfiType = RustBuffer;
+
+    fn try_read(buf: &mut &[u8]) -> Result<Vec<T>> {
+        check_remaining(buf, 4)?;
+        let len = usize::try_from(buf.get_i32())?;
+        let mut vec = Vec::with_capacity(len);
+        for _ in 0..len {
+            vec.push(<T as Lift<UT>>::try_read(buf)?)
+        }
+        Ok(vec)
+    }
+
+    fn try_lift(buf: RustBuffer) -> Result<Vec<T>> {
+        Self::try_lift_from_rust_buffer(buf)
+    }
+
+    const TYPE_ID_META: MetadataBuffer =
+        MetadataBuffer::from_code(metadata::codes::TYPE_VEC).concat(T::TYPE_ID_META);
+}
+
+unsafe impl<K, V, UT> Lower<UT> for HashMap<K, V>
+where
+    K: Lower<UT> + std::hash::Hash + Eq,
+    V: Lower<UT>,
+{
+    type FfiType = RustBuffer;
+
+    fn write(obj: HashMap<K, V>, buf: &mut Vec<u8>) {
+        // TODO: would be nice not to panic here :-/
+        let len = i32::try_from(obj.len()).unwrap();
+        buf.put_i32(len); // We limit HashMaps to i32::MAX entries
+        for (key, value) in obj {
+            <K as Lower<UT>>::write(key, buf);
+            <V as Lower<UT>>::write(value, buf);
+        }
+    }
+
+    fn lower(obj: HashMap<K, V>) -> RustBuffer {
+        Self::lower_into_rust_buffer(obj)
+    }
+
+    const TYPE_ID_META: MetadataBuffer = MetadataBuffer::from_code(metadata::codes::TYPE_HASH_MAP)
+        .concat(K::TYPE_ID_META)
+        .concat(V::TYPE_ID_META);
+}
+
+unsafe impl<K, V, UT> Lift<UT> for HashMap<K, V>
+where
+    K: Lift<UT> + std::hash::Hash + Eq,
+    V: Lift<UT>,
+{
+    type FfiType = RustBuffer;
+
+    fn try_read(buf: &mut &[u8]) -> Result<HashMap<K, V>> {
+        check_remaining(buf, 4)?;
+        let len = usize::try_from(buf.get_i32())?;
+        let mut map = HashMap::with_capacity(len);
+        for _ in 0..len {
+            let key = <K as Lift<UT>>::try_read(buf)?;
+            let value = <V as Lift<UT>>::try_read(buf)?;
+            map.insert(key, value);
+        }
+        Ok(map)
+    }
+
+    fn try_lift(buf: RustBuffer) -> Result<HashMap<K, V>> {
+        Self::try_lift_from_rust_buffer(buf)
+    }
+
+    const TYPE_ID_META: MetadataBuffer = MetadataBuffer::from_code(metadata::codes::TYPE_HASH_MAP)
+        .concat(K::TYPE_ID_META)
+        .concat(V::TYPE_ID_META);
+}
+
+derive_ffi_traits!(blanket u8);
+derive_ffi_traits!(blanket i8);
+derive_ffi_traits!(blanket u16);
+derive_ffi_traits!(blanket i16);
+derive_ffi_traits!(blanket u32);
+derive_ffi_traits!(blanket i32);
+derive_ffi_traits!(blanket u64);
+derive_ffi_traits!(blanket i64);
+derive_ffi_traits!(blanket f32);
+derive_ffi_traits!(blanket f64);
+derive_ffi_traits!(blanket bool);
+derive_ffi_traits!(blanket String);
+derive_ffi_traits!(blanket Duration);
+derive_ffi_traits!(blanket SystemTime);
+
+// For composite types, derive LowerReturn, LiftReturn, etc, from Lift/Lower.
+//
+// Note that this means we don't get specialized return handling.  For example, if we could return
+// an `Option<Result<>>` we would always return that type directly and never throw.
+derive_ffi_traits!(impl<T, UT> LowerReturn<UT> for Option<T> where Option<T>: Lower<UT>);
+derive_ffi_traits!(impl<T, UT> LiftReturn<UT> for Option<T> where Option<T>: Lift<UT>);
+derive_ffi_traits!(impl<T, UT> LiftRef<UT> for Option<T> where Option<T>: Lift<UT>);
+
+derive_ffi_traits!(impl<T, UT> LowerReturn<UT> for Vec<T> where Vec<T>: Lower<UT>);
+derive_ffi_traits!(impl<T, UT> LiftReturn<UT> for Vec<T> where Vec<T>: Lift<UT>);
+derive_ffi_traits!(impl<T, UT> LiftRef<UT> for Vec<T> where Vec<T>: Lift<UT>);
+
+derive_ffi_traits!(impl<K, V, UT> LowerReturn<UT> for HashMap<K, V> where HashMap<K, V>: Lower<UT>);
+derive_ffi_traits!(impl<K, V, UT> LiftReturn<UT> for HashMap<K, V> where HashMap<K, V>: Lift<UT>);
+derive_ffi_traits!(impl<K, V, UT> LiftRef<UT> for HashMap<K, V> where HashMap<K, V>: Lift<UT>);
+
+// For Arc we derive all the traits, but have to write it all out because we need an unsized T bound
+derive_ffi_traits!(impl<T, UT> Lower<UT> for Arc<T> where Arc<T>: FfiConverter<UT>, T: ?Sized);
+derive_ffi_traits!(impl<T, UT> Lift<UT> for Arc<T> where Arc<T>: FfiConverter<UT>, T: ?Sized);
+derive_ffi_traits!(impl<T, UT> LowerReturn<UT> for Arc<T> where Arc<T>: Lower<UT>, T: ?Sized);
+derive_ffi_traits!(impl<T, UT> LiftReturn<UT> for Arc<T> where Arc<T>: Lift<UT>, T: ?Sized);
+derive_ffi_traits!(impl<T, UT> LiftRef<UT> for Arc<T> where Arc<T>: Lift<UT>, T: ?Sized);
+
+// Implement LowerReturn/LiftReturn for the unit type (void returns)
+
+unsafe impl<UT> LowerReturn<UT> for () {
+    type ReturnType = ();
+
+    fn lower_return(_: ()) -> Result<Self::ReturnType, RustBuffer> {
+        Ok(())
+    }
+
+    const TYPE_ID_META: MetadataBuffer = MetadataBuffer::from_code(metadata::codes::TYPE_UNIT);
+}
+
+unsafe impl<UT> LiftReturn<UT> for () {
+    fn lift_callback_return(_buf: RustBuffer) -> Self {}
+
+    const TYPE_ID_META: MetadataBuffer = MetadataBuffer::from_code(metadata::codes::TYPE_UNIT);
+}
+
+// Implement LowerReturn/LiftReturn for `Result<R, E>`.  This is where we handle exceptions/Err
+// results.
+
+unsafe impl<UT, R, E> LowerReturn<UT> for Result<R, E>
+where
+    R: LowerReturn<UT>,
+    E: Lower<UT> + Error + Send + Sync + 'static,
+{
+    type ReturnType = R::ReturnType;
+
+    fn lower_return(v: Self) -> Result<Self::ReturnType, RustBuffer> {
+        match v {
+            Ok(r) => R::lower_return(r),
+            Err(e) => Err(E::lower_into_rust_buffer(e)),
+        }
+    }
+
+    fn handle_failed_lift(arg_name: &str, err: anyhow::Error) -> Self {
+        match err.downcast::<E>() {
+            Ok(actual_error) => Err(actual_error),
+            Err(ohno) => panic!("Failed to convert arg '{arg_name}': {ohno}"),
+        }
+    }
+
+    const TYPE_ID_META: MetadataBuffer = MetadataBuffer::from_code(metadata::codes::TYPE_RESULT)
+        .concat(R::TYPE_ID_META)
+        .concat(E::TYPE_ID_META);
+}
+
+unsafe impl<UT, R, E> LiftReturn<UT> for Result<R, E>
+where
+    R: LiftReturn<UT>,
+    E: Lift<UT> + ConvertError<UT>,
+{
+    fn lift_callback_return(buf: RustBuffer) -> Self {
+        Ok(R::lift_callback_return(buf))
+    }
+
+    fn lift_callback_error(buf: RustBuffer) -> Self {
+        match E::try_lift_from_rust_buffer(buf) {
+            Ok(lifted_error) => Err(lifted_error),
+            Err(anyhow_error) => {
+                Self::handle_callback_unexpected_error(UnexpectedUniFFICallbackError {
+                    reason: format!("Error lifting from rust buffer: {anyhow_error}"),
+                })
+            }
+        }
+    }
+
+    fn handle_callback_unexpected_error(e: UnexpectedUniFFICallbackError) -> Self {
+        Err(E::try_convert_unexpected_callback_error(e).unwrap_or_else(|e| panic!("{e}")))
+    }
+
+    const TYPE_ID_META: MetadataBuffer = MetadataBuffer::from_code(metadata::codes::TYPE_RESULT)
+        .concat(R::TYPE_ID_META)
+        .concat(E::TYPE_ID_META);
+}
+
+unsafe impl<T, UT> LiftRef<UT> for [T]
+where
+    T: Lift<UT>,
+{
+    type LiftType = Vec<T>;
+}
+
+unsafe impl<UT> LiftRef<UT> for str {
+    type LiftType = String;
+}
diff --git a/src/ffi_converter_traits.rs b/src/ffi_converter_traits.rs
new file mode 100644
index 0000000..3b5914e
--- /dev/null
+++ b/src/ffi_converter_traits.rs
@@ -0,0 +1,466 @@
+/* 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/. */
+
+//! Traits that define how to transfer values via the FFI layer.
+//!
+//! These traits define how to pass values over the FFI in various ways: as arguments or as return
+//! values, from Rust to the foreign side and vice-versa.  These traits are mainly used by the
+//! proc-macro generated code.  The goal is to allow the proc-macros to go from a type name to the
+//! correct function for a given FFI operation.
+//!
+//! The traits form a sort-of tree structure from general to specific:
+//! ```ignore
+//!
+//!                   [FfiConverter]
+//!                        |
+//!           -----------------------------
+//!           |                           |
+//!       [Lower]                      [Lift]
+//!           |                           |
+//!           |                       --------------
+//!           |                       |            |
+//!       [LowerReturn]           [LiftRef]  [LiftReturn]
+//! ```
+//!
+//! The `derive_ffi_traits` macro can be used to derive the specific traits from the general ones.
+//! Here's the main ways we implement these traits:
+//!
+//! * For most types we implement [FfiConverter] and use [derive_ffi_traits] to implement the rest
+//! * If a type can only be lifted/lowered, then we implement [Lift] or [Lower] and use
+//!   [derive_ffi_traits] to implement the rest
+//! * If a type needs special-case handling, like `Result<>` and `()`, we implement the traits
+//!   directly.
+//!
+//! FfiConverter has a generic parameter, that's filled in with a type local to the UniFFI consumer crate.
+//! This allows us to work around the Rust orphan rules for remote types. See
+//! `https://mozilla.github.io/uniffi-rs/internals/lifting_and_lowering.html#code-generation-and-the-fficonverter-trait`
+//! for details.
+//!
+//! ## Safety
+//!
+//! All traits are unsafe (implementing it requires `unsafe impl`) because we can't guarantee
+//! that it's safe to pass your type out to foreign-language code and back again. Buggy
+//! implementations of this trait might violate some assumptions made by the generated code,
+//! or might not match with the corresponding code in the generated foreign-language bindings.
+//! These traits should not be used directly, only in generated code, and the generated code should
+//! have fixture tests to test that everything works correctly together.
+
+use std::{borrow::Borrow, sync::Arc};
+
+use anyhow::bail;
+use bytes::Buf;
+
+use crate::{FfiDefault, MetadataBuffer, Result, RustBuffer, UnexpectedUniFFICallbackError};
+
+/// Generalized FFI conversions
+///
+/// This trait is not used directly by the code generation, but implement this and calling
+/// [derive_ffi_traits] is a simple way to implement all the traits that are.
+///
+/// ## Safety
+///
+/// All traits are unsafe (implementing it requires `unsafe impl`) because we can't guarantee
+/// that it's safe to pass your type out to foreign-language code and back again. Buggy
+/// implementations of this trait might violate some assumptions made by the generated code,
+/// or might not match with the corresponding code in the generated foreign-language bindings.
+/// These traits should not be used directly, only in generated code, and the generated code should
+/// have fixture tests to test that everything works correctly together.
+pub unsafe trait FfiConverter<UT>: Sized {
+    /// The low-level type used for passing values of this type over the FFI.
+    ///
+    /// This must be a C-compatible type (e.g. a numeric primitive, a `#[repr(C)]` struct) into
+    /// which values of the target rust type can be converted.
+    ///
+    /// For complex data types, we currently recommend using `RustBuffer` and serializing
+    /// the data for transfer. In theory it could be possible to build a matching
+    /// `#[repr(C)]` struct for a complex data type and pass that instead, but explicit
+    /// serialization is simpler and safer as a starting point.
+    ///
+    /// If a type implements multiple FFI traits, `FfiType` must be the same for all of them.
+    type FfiType: FfiDefault;
+
+    /// Lower a rust value of the target type, into an FFI value of type Self::FfiType.
+    ///
+    /// This trait method is used for sending data from rust to the foreign language code,
+    /// by (hopefully cheaply!) converting it into something that can be passed over the FFI
+    /// and reconstructed on the other side.
+    ///
+    /// Note that this method takes an owned value; this allows it to transfer ownership in turn to
+    /// the foreign language code, e.g. by boxing the value and passing a pointer.
+    fn lower(obj: Self) -> Self::FfiType;
+
+    /// Lift a rust value of the target type, from an FFI value of type Self::FfiType.
+    ///
+    /// This trait method is used for receiving data from the foreign language code in rust,
+    /// by (hopefully cheaply!) converting it from a low-level FFI value of type Self::FfiType
+    /// into a high-level rust value of the target type.
+    ///
+    /// Since we cannot statically guarantee that the foreign-language code will send valid
+    /// values of type Self::FfiType, this method is fallible.
+    fn try_lift(v: Self::FfiType) -> Result<Self>;
+
+    /// Write a rust value into a buffer, to send over the FFI in serialized form.
+    ///
+    /// This trait method can be used for sending data from rust to the foreign language code,
+    /// in cases where we're not able to use a special-purpose FFI type and must fall back to
+    /// sending serialized bytes.
+    ///
+    /// Note that this method takes an owned value because it's transferring ownership
+    /// to the foreign language code via the RustBuffer.
+    fn write(obj: Self, buf: &mut Vec<u8>);
+
+    /// Read a rust value from a buffer, received over the FFI in serialized form.
+    ///
+    /// This trait method can be used for receiving data from the foreign language code in rust,
+    /// in cases where we're not able to use a special-purpose FFI type and must fall back to
+    /// receiving serialized bytes.
+    ///
+    /// Since we cannot statically guarantee that the foreign-language code will send valid
+    /// serialized bytes for the target type, this method is fallible.
+    ///
+    /// Note the slightly unusual type here - we want a mutable reference to a slice of bytes,
+    /// because we want to be able to advance the start of the slice after reading an item
+    /// from it (but will not mutate the actual contents of the slice).
+    fn try_read(buf: &mut &[u8]) -> Result<Self>;
+
+    /// Type ID metadata, serialized into a [MetadataBuffer].
+    ///
+    /// If a type implements multiple FFI traits, `TYPE_ID_META` must be the same for all of them.
+    const TYPE_ID_META: MetadataBuffer;
+}
+
+/// FfiConverter for Arc-types
+///
+/// This trait gets around the orphan rule limitations, which prevent library crates from
+/// implementing `FfiConverter` on an Arc. When this is implemented for T, we generate an
+/// `FfiConverter` impl for Arc<T>.
+///
+/// Note: There's no need for `FfiConverterBox`, since Box is a fundamental type.
+///
+/// ## Safety
+///
+/// All traits are unsafe (implementing it requires `unsafe impl`) because we can't guarantee
+/// that it's safe to pass your type out to foreign-language code and back again. Buggy
+/// implementations of this trait might violate some assumptions made by the generated code,
+/// or might not match with the corresponding code in the generated foreign-language bindings.
+/// These traits should not be used directly, only in generated code, and the generated code should
+/// have fixture tests to test that everything works correctly together.
+pub unsafe trait FfiConverterArc<UT>: Send + Sync {
+    type FfiType: FfiDefault;
+
+    fn lower(obj: Arc<Self>) -> Self::FfiType;
+    fn try_lift(v: Self::FfiType) -> Result<Arc<Self>>;
+    fn write(obj: Arc<Self>, buf: &mut Vec<u8>);
+    fn try_read(buf: &mut &[u8]) -> Result<Arc<Self>>;
+
+    const TYPE_ID_META: MetadataBuffer;
+}
+
+unsafe impl<T, UT> FfiConverter<UT> for Arc<T>
+where
+    T: FfiConverterArc<UT> + ?Sized,
+{
+    type FfiType = T::FfiType;
+
+    fn lower(obj: Self) -> Self::FfiType {
+        T::lower(obj)
+    }
+
+    fn try_lift(v: Self::FfiType) -> Result<Self> {
+        T::try_lift(v)
+    }
+
+    fn write(obj: Self, buf: &mut Vec<u8>) {
+        T::write(obj, buf)
+    }
+
+    fn try_read(buf: &mut &[u8]) -> Result<Self> {
+        T::try_read(buf)
+    }
+
+    const TYPE_ID_META: MetadataBuffer = T::TYPE_ID_META;
+}
+
+/// Lift values passed by the foreign code over the FFI into Rust values
+///
+/// This is used by the code generation to handle arguments.  It's usually derived from
+/// [FfiConverter], except for types that only support lifting but not lowering.
+///
+/// See [FfiConverter] for a discussion of the methods
+///
+/// ## Safety
+///
+/// All traits are unsafe (implementing it requires `unsafe impl`) because we can't guarantee
+/// that it's safe to pass your type out to foreign-language code and back again. Buggy
+/// implementations of this trait might violate some assumptions made by the generated code,
+/// or might not match with the corresponding code in the generated foreign-language bindings.
+/// These traits should not be used directly, only in generated code, and the generated code should
+/// have fixture tests to test that everything works correctly together.
+pub unsafe trait Lift<UT>: Sized {
+    type FfiType;
+
+    fn try_lift(v: Self::FfiType) -> Result<Self>;
+
+    fn try_read(buf: &mut &[u8]) -> Result<Self>;
+
+    /// Convenience method
+    fn try_lift_from_rust_buffer(v: RustBuffer) -> Result<Self> {
+        let vec = v.destroy_into_vec();
+        let mut buf = vec.as_slice();
+        let value = Self::try_read(&mut buf)?;
+        match Buf::remaining(&buf) {
+            0 => Ok(value),
+            n => bail!("junk data left in buffer after lifting (count: {n})",),
+        }
+    }
+
+    const TYPE_ID_META: MetadataBuffer;
+}
+
+/// Lower Rust values to pass them to the foreign code
+///
+/// This is used to pass arguments to callback interfaces. It's usually derived from
+/// [FfiConverter], except for types that only support lowering but not lifting.
+///
+/// See [FfiConverter] for a discussion of the methods
+///
+/// ## Safety
+///
+/// All traits are unsafe (implementing it requires `unsafe impl`) because we can't guarantee
+/// that it's safe to pass your type out to foreign-language code and back again. Buggy
+/// implementations of this trait might violate some assumptions made by the generated code,
+/// or might not match with the corresponding code in the generated foreign-language bindings.
+/// These traits should not be used directly, only in generated code, and the generated code should
+/// have fixture tests to test that everything works correctly together.
+pub unsafe trait Lower<UT>: Sized {
+    type FfiType: FfiDefault;
+
+    fn lower(obj: Self) -> Self::FfiType;
+
+    fn write(obj: Self, buf: &mut Vec<u8>);
+
+    /// Convenience method
+    fn lower_into_rust_buffer(obj: Self) -> RustBuffer {
+        let mut buf = ::std::vec::Vec::new();
+        Self::write(obj, &mut buf);
+        RustBuffer::from_vec(buf)
+    }
+
+    const TYPE_ID_META: MetadataBuffer;
+}
+
+/// Return Rust values to the foreign code
+///
+/// This is usually derived from [Lift], but we special case types like `Result<>` and `()`.
+///
+/// ## Safety
+///
+/// All traits are unsafe (implementing it requires `unsafe impl`) because we can't guarantee
+/// that it's safe to pass your type out to foreign-language code and back again. Buggy
+/// implementations of this trait might violate some assumptions made by the generated code,
+/// or might not match with the corresponding code in the generated foreign-language bindings.
+/// These traits should not be used directly, only in generated code, and the generated code should
+/// have fixture tests to test that everything works correctly together.
+pub unsafe trait LowerReturn<UT>: Sized {
+    /// The type that should be returned by scaffolding functions for this type.
+    ///
+    /// When derived, it's the same as `FfiType`.
+    type ReturnType: FfiDefault;
+
+    /// Lower this value for scaffolding function return
+    ///
+    /// This method converts values into the `Result<>` type that [rust_call] expects. For
+    /// successful calls, return `Ok(lower_return)`.  For errors that should be translated into
+    /// thrown exceptions on the foreign code, serialize the error into a RustBuffer and return
+    /// `Err(buf)`
+    fn lower_return(obj: Self) -> Result<Self::ReturnType, RustBuffer>;
+
+    /// If possible, get a serialized error for failed argument lifts
+    ///
+    /// By default, we just panic and let `rust_call` handle things.  However, for `Result<_, E>`
+    /// returns, if the anyhow error can be downcast to `E`, then serialize that and return it.
+    /// This results in the foreign code throwing a "normal" exception, rather than an unexpected
+    /// exception.
+    fn handle_failed_lift(arg_name: &str, e: anyhow::Error) -> Self {
+        panic!("Failed to convert arg '{arg_name}': {e}")
+    }
+
+    const TYPE_ID_META: MetadataBuffer;
+}
+
+/// Return foreign values to Rust
+///
+/// This is usually derived from [Lower], but we special case types like `Result<>` and `()`.
+///
+/// ## Safety
+///
+/// All traits are unsafe (implementing it requires `unsafe impl`) because we can't guarantee
+/// that it's safe to pass your type out to foreign-language code and back again. Buggy
+/// implementations of this trait might violate some assumptions made by the generated code,
+/// or might not match with the corresponding code in the generated foreign-language bindings.
+/// These traits should not be used directly, only in generated code, and the generated code should
+/// have fixture tests to test that everything works correctly together.
+pub unsafe trait LiftReturn<UT>: Sized {
+    /// Lift a Rust value for a callback interface method result
+    fn lift_callback_return(buf: RustBuffer) -> Self;
+
+    /// Lift a Rust value for a callback interface method error result
+    ///
+    /// This is called for "expected errors" -- the callback method returns a Result<> type and the
+    /// foreign code throws an exception that corresponds to the error type.
+    fn lift_callback_error(_buf: RustBuffer) -> Self {
+        panic!("Callback interface method returned unexpected error")
+    }
+
+    /// Lift a Rust value for an unexpected callback interface error
+    ///
+    /// The main reason this is called is when the callback interface throws an error type that
+    /// doesn't match the Rust trait definition.  It's also called for corner cases, like when the
+    /// foreign code doesn't follow the FFI contract.
+    ///
+    /// The default implementation panics unconditionally.  Errors used in callback interfaces
+    /// handle this using the `From<UnexpectedUniFFICallbackError>` impl that the library author
+    /// must provide.
+    fn handle_callback_unexpected_error(e: UnexpectedUniFFICallbackError) -> Self {
+        panic!("Callback interface failure: {e}")
+    }
+
+    const TYPE_ID_META: MetadataBuffer;
+}
+
+/// Lift references
+///
+/// This is usually derived from [Lift] and also implemented for the inner `T` value of smart
+/// pointers.  For example, if `Lift` is implemented for `Arc<T>`, then we implement this to lift
+///
+/// ## Safety
+///
+/// All traits are unsafe (implementing it requires `unsafe impl`) because we can't guarantee
+/// that it's safe to pass your type out to foreign-language code and back again. Buggy
+/// implementations of this trait might violate some assumptions made by the generated code,
+/// or might not match with the corresponding code in the generated foreign-language bindings.
+/// These traits should not be used directly, only in generated code, and the generated code should
+/// have fixture tests to test that everything works correctly together.
+/// `&T` using the Arc.
+pub unsafe trait LiftRef<UT> {
+    type LiftType: Lift<UT> + Borrow<Self>;
+}
+
+pub trait ConvertError<UT>: Sized {
+    fn try_convert_unexpected_callback_error(e: UnexpectedUniFFICallbackError) -> Result<Self>;
+}
+
+/// Derive FFI traits
+///
+/// This can be used to derive:
+///   * [Lower] and [Lift] from [FfiConverter]
+///   * [LowerReturn] from [Lower]
+///   * [LiftReturn] and [LiftRef] from [Lift]
+///
+/// Usage:
+/// ```ignore
+///
+/// // Derive everything from [FfiConverter] for all Uniffi tags
+/// ::uniffi::derive_ffi_traits!(blanket Foo)
+/// // Derive everything from [FfiConverter] for the local crate::UniFfiTag
+/// ::uniffi::derive_ffi_traits!(local Foo)
+/// // To derive a specific trait, write out the impl item minus the actual  block
+/// ::uniffi::derive_ffi_traits!(impl<T, UT> LowerReturn<UT> for Option<T>)
+/// ```
+#[macro_export]
+#[allow(clippy::crate_in_macro_def)]
+macro_rules! derive_ffi_traits {
+    (blanket $ty:ty) => {
+        $crate::derive_ffi_traits!(impl<UT> Lower<UT> for $ty);
+        $crate::derive_ffi_traits!(impl<UT> Lift<UT> for $ty);
+        $crate::derive_ffi_traits!(impl<UT> LowerReturn<UT> for $ty);
+        $crate::derive_ffi_traits!(impl<UT> LiftReturn<UT> for $ty);
+        $crate::derive_ffi_traits!(impl<UT> LiftRef<UT> for $ty);
+        $crate::derive_ffi_traits!(impl<UT> ConvertError<UT> for $ty);
+    };
+
+    (local $ty:ty) => {
+        $crate::derive_ffi_traits!(impl Lower<crate::UniFfiTag> for $ty);
+        $crate::derive_ffi_traits!(impl Lift<crate::UniFfiTag> for $ty);
+        $crate::derive_ffi_traits!(impl LowerReturn<crate::UniFfiTag> for $ty);
+        $crate::derive_ffi_traits!(impl LiftReturn<crate::UniFfiTag> for $ty);
+        $crate::derive_ffi_traits!(impl LiftRef<crate::UniFfiTag> for $ty);
+        $crate::derive_ffi_traits!(impl ConvertError<crate::UniFfiTag> for $ty);
+    };
+
+    (impl $(<$($generic:ident),*>)? $(::uniffi::)? Lower<$ut:path> for $ty:ty $(where $($where:tt)*)?) => {
+        unsafe impl $(<$($generic),*>)* $crate::Lower<$ut> for $ty $(where $($where)*)*
+        {
+            type FfiType = <Self as $crate::FfiConverter<$ut>>::FfiType;
+
+            fn lower(obj: Self) -> Self::FfiType {
+                <Self as $crate::FfiConverter<$ut>>::lower(obj)
+            }
+
+            fn write(obj: Self, buf: &mut ::std::vec::Vec<u8>) {
+                <Self as $crate::FfiConverter<$ut>>::write(obj, buf)
+            }
+
+            const TYPE_ID_META: $crate::MetadataBuffer = <Self as $crate::FfiConverter<$ut>>::TYPE_ID_META;
+        }
+    };
+
+    (impl $(<$($generic:ident),*>)? $(::uniffi::)? Lift<$ut:path> for $ty:ty $(where $($where:tt)*)?) => {
+        unsafe impl $(<$($generic),*>)* $crate::Lift<$ut> for $ty $(where $($where)*)*
+        {
+            type FfiType = <Self as $crate::FfiConverter<$ut>>::FfiType;
+
+            fn try_lift(v: Self::FfiType) -> $crate::deps::anyhow::Result<Self> {
+                <Self as $crate::FfiConverter<$ut>>::try_lift(v)
+            }
+
+            fn try_read(buf: &mut &[u8]) -> $crate::deps::anyhow::Result<Self> {
+                <Self as $crate::FfiConverter<$ut>>::try_read(buf)
+            }
+
+            const TYPE_ID_META: $crate::MetadataBuffer = <Self as $crate::FfiConverter<$ut>>::TYPE_ID_META;
+        }
+    };
+
+    (impl $(<$($generic:ident),*>)? $(::uniffi::)? LowerReturn<$ut:path> for $ty:ty $(where $($where:tt)*)?) => {
+        unsafe impl $(<$($generic),*>)* $crate::LowerReturn<$ut> for $ty $(where $($where)*)*
+        {
+            type ReturnType = <Self as $crate::Lower<$ut>>::FfiType;
+
+            fn lower_return(obj: Self) -> $crate::deps::anyhow::Result<Self::ReturnType, $crate::RustBuffer> {
+                Ok(<Self as $crate::Lower<$ut>>::lower(obj))
+            }
+
+            const TYPE_ID_META: $crate::MetadataBuffer =<Self as $crate::Lower<$ut>>::TYPE_ID_META;
+        }
+    };
+
+    (impl $(<$($generic:ident),*>)? $(::uniffi::)? LiftReturn<$ut:path> for $ty:ty $(where $($where:tt)*)?) => {
+        unsafe impl $(<$($generic),*>)* $crate::LiftReturn<$ut> for $ty $(where $($where)*)*
+        {
+            fn lift_callback_return(buf: $crate::RustBuffer) -> Self {
+                <Self as $crate::Lift<$ut>>::try_lift_from_rust_buffer(buf)
+                    .expect("Error reading callback interface result")
+            }
+
+            const TYPE_ID_META: $crate::MetadataBuffer = <Self as $crate::Lift<$ut>>::TYPE_ID_META;
+        }
+    };
+
+    (impl $(<$($generic:ident),*>)? $(::uniffi::)? LiftRef<$ut:path> for $ty:ty $(where $($where:tt)*)?) => {
+        unsafe impl $(<$($generic),*>)* $crate::LiftRef<$ut> for $ty $(where $($where)*)*
+        {
+            type LiftType = Self;
+        }
+    };
+
+    (impl $(<$($generic:ident),*>)? $(::uniffi::)? ConvertError<$ut:path> for $ty:ty $(where $($where:tt)*)?) => {
+        impl $(<$($generic),*>)* $crate::ConvertError<$ut> for $ty $(where $($where)*)*
+        {
+            fn try_convert_unexpected_callback_error(e: $crate::UnexpectedUniFFICallbackError) -> $crate::deps::anyhow::Result<Self> {
+                $crate::convert_unexpected_error!(e, $ty)
+            }
+        }
+    };
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..9003b08
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,322 @@
+/* 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/. */
+
+//! # Runtime support code for uniffi
+//!
+//! This crate provides the small amount of runtime code that is required by the generated uniffi
+//! component scaffolding in order to transfer data back and forth across the C-style FFI layer,
+//! as well as some utilities for testing the generated bindings.
+//!
+//! The key concept here is the [`FfiConverter`] trait, which is responsible for converting between
+//! a Rust type and a low-level C-style type that can be passed across the FFI:
+//!
+//!  * How to [represent](FfiConverter::FfiType) values of the Rust type in the low-level C-style type
+//!    system of the FFI layer.
+//!  * How to ["lower"](FfiConverter::lower) values of the Rust type into an appropriate low-level
+//!    FFI value.
+//!  * How to ["lift"](FfiConverter::try_lift) low-level FFI values back into values of the Rust
+//!    type.
+//!  * How to [write](FfiConverter::write) values of the Rust type into a buffer, for cases
+//!    where they are part of a compound data structure that is serialized for transfer.
+//!  * How to [read](FfiConverter::try_read) values of the Rust type from buffer, for cases
+//!    where they are received as part of a compound data structure that was serialized for transfer.
+//!  * How to [return](FfiConverter::lower_return) values of the Rust type from scaffolding
+//!    functions.
+//!
+//! This logic encapsulates the Rust-side handling of data transfer. Each foreign-language binding
+//! must also implement a matching set of data-handling rules for each data type.
+//!
+//! In addition to the core `FfiConverter` trait, we provide a handful of struct definitions useful
+//! for passing core rust types over the FFI, such as [`RustBuffer`].
+
+#![warn(rust_2018_idioms, unused_qualifications)]
+
+use anyhow::bail;
+use bytes::buf::Buf;
+
+// Make Result<> public to support external impls of FfiConverter
+pub use anyhow::Result;
+
+pub mod ffi;
+mod ffi_converter_impls;
+mod ffi_converter_traits;
+pub mod metadata;
+
+pub use ffi::*;
+pub use ffi_converter_traits::{
+    ConvertError, FfiConverter, FfiConverterArc, Lift, LiftRef, LiftReturn, Lower, LowerReturn,
+};
+pub use metadata::*;
+
+// Re-export the libs that we use in the generated code,
+// so the consumer doesn't have to depend on them directly.
+pub mod deps {
+    pub use anyhow;
+    #[cfg(feature = "tokio")]
+    pub use async_compat;
+    pub use bytes;
+    pub use log;
+    pub use static_assertions;
+}
+
+mod panichook;
+
+const PACKAGE_VERSION: &str = env!("CARGO_PKG_VERSION");
+
+// For the significance of this magic number 10 here, and the reason that
+// it can't be a named constant, see the `check_compatible_version` function.
+static_assertions::const_assert!(PACKAGE_VERSION.as_bytes().len() < 10);
+
+/// Check whether the uniffi runtime version is compatible a given uniffi_bindgen version.
+///
+/// The result of this check may be used to ensure that generated Rust scaffolding is
+/// using a compatible version of the uniffi runtime crate. It's a `const fn` so that it
+/// can be used to perform such a check at compile time.
+#[allow(clippy::len_zero)]
+pub const fn check_compatible_version(bindgen_version: &'static str) -> bool {
+    // While UniFFI is still under heavy development, we require that
+    // the runtime support crate be precisely the same version as the
+    // build-time bindgen crate.
+    //
+    // What we want to achieve here is checking two strings for equality.
+    // Unfortunately Rust doesn't yet support calling the `&str` equals method
+    // in a const context. We can hack around that by doing a byte-by-byte
+    // comparison of the underlying bytes.
+    let package_version = PACKAGE_VERSION.as_bytes();
+    let bindgen_version = bindgen_version.as_bytes();
+    // What we want to achieve here is a loop over the underlying bytes,
+    // something like:
+    // ```
+    //  if package_version.len() != bindgen_version.len() {
+    //      return false
+    //  }
+    //  for i in 0..package_version.len() {
+    //      if package_version[i] != bindgen_version[i] {
+    //          return false
+    //      }
+    //  }
+    //  return true
+    // ```
+    // Unfortunately stable Rust doesn't allow `if` or `for` in const contexts,
+    // so code like the above would only work in nightly. We can hack around it by
+    // statically asserting that the string is shorter than a certain length
+    // (currently 10 bytes) and then manually unrolling that many iterations of the loop.
+    //
+    // Yes, I am aware that this is horrific, but the externally-visible
+    // behaviour is quite nice for consumers!
+    package_version.len() == bindgen_version.len()
+        && (package_version.len() == 0 || package_version[0] == bindgen_version[0])
+        && (package_version.len() <= 1 || package_version[1] == bindgen_version[1])
+        && (package_version.len() <= 2 || package_version[2] == bindgen_version[2])
+        && (package_version.len() <= 3 || package_version[3] == bindgen_version[3])
+        && (package_version.len() <= 4 || package_version[4] == bindgen_version[4])
+        && (package_version.len() <= 5 || package_version[5] == bindgen_version[5])
+        && (package_version.len() <= 6 || package_version[6] == bindgen_version[6])
+        && (package_version.len() <= 7 || package_version[7] == bindgen_version[7])
+        && (package_version.len() <= 8 || package_version[8] == bindgen_version[8])
+        && (package_version.len() <= 9 || package_version[9] == bindgen_version[9])
+        && package_version.len() < 10
+}
+
+/// Assert that the uniffi runtime version matches an expected value.
+///
+/// This is a helper hook for the generated Rust scaffolding, to produce a compile-time
+/// error if the version of `uniffi_bindgen` used to generate the scaffolding was
+/// incompatible with the version of `uniffi` being used at runtime.
+#[macro_export]
+macro_rules! assert_compatible_version {
+    ($v:expr $(,)?) => {
+        uniffi::deps::static_assertions::const_assert!(uniffi::check_compatible_version($v));
+    };
+}
+
+/// Struct to use when we want to lift/lower/serialize types inside the `uniffi` crate.
+struct UniFfiTag;
+
+/// A helper function to ensure we don't read past the end of a buffer.
+///
+/// Rust won't actually let us read past the end of a buffer, but the `Buf` trait does not support
+/// returning an explicit error in this case, and will instead panic. This is a look-before-you-leap
+/// helper function to instead return an explicit error, to help with debugging.
+pub fn check_remaining(buf: &[u8], num_bytes: usize) -> Result<()> {
+    if buf.remaining() < num_bytes {
+        bail!(
+            "not enough bytes remaining in buffer ({} < {num_bytes})",
+            buf.remaining(),
+        );
+    }
+    Ok(())
+}
+
+/// Macro to implement lowering/lifting using a `RustBuffer`
+///
+/// For complex types where it's too fiddly or too unsafe to convert them into a special-purpose
+/// C-compatible value, you can use this trait to implement `lower()` in terms of `write()` and
+/// `lift` in terms of `read()`.
+///
+/// This macro implements the boilerplate needed to define `lower`, `lift` and `FFIType`.
+#[macro_export]
+macro_rules! ffi_converter_rust_buffer_lift_and_lower {
+    ($uniffi_tag:ty) => {
+        type FfiType = $crate::RustBuffer;
+
+        fn lower(v: Self) -> $crate::RustBuffer {
+            let mut buf = ::std::vec::Vec::new();
+            <Self as $crate::FfiConverter<$uniffi_tag>>::write(v, &mut buf);
+            $crate::RustBuffer::from_vec(buf)
+        }
+
+        fn try_lift(buf: $crate::RustBuffer) -> $crate::Result<Self> {
+            let vec = buf.destroy_into_vec();
+            let mut buf = vec.as_slice();
+            let value = <Self as $crate::FfiConverter<$uniffi_tag>>::try_read(&mut buf)?;
+            match $crate::deps::bytes::Buf::remaining(&buf) {
+                0 => Ok(value),
+                n => $crate::deps::anyhow::bail!(
+                    "junk data left in buffer after lifting (count: {n})",
+                ),
+            }
+        }
+    };
+}
+
+/// Macro to implement `FfiConverter<T>` for a UniFfiTag using a different UniFfiTag
+///
+/// This is used for external types
+#[macro_export]
+macro_rules! ffi_converter_forward {
+    // Forward a `FfiConverter` implementation
+    ($T:ty, $existing_impl_tag:ty, $new_impl_tag:ty) => {
+        ::uniffi::do_ffi_converter_forward!(
+            FfiConverter,
+            $T,
+            $T,
+            $existing_impl_tag,
+            $new_impl_tag
+        );
+
+        $crate::derive_ffi_traits!(local $T);
+    };
+}
+
+/// Macro to implement `FfiConverterArc<T>` for a UniFfiTag using a different UniFfiTag
+///
+/// This is used for external types
+#[macro_export]
+macro_rules! ffi_converter_arc_forward {
+    ($T:ty, $existing_impl_tag:ty, $new_impl_tag:ty) => {
+        ::uniffi::do_ffi_converter_forward!(
+            FfiConverterArc,
+            ::std::sync::Arc<$T>,
+            $T,
+            $existing_impl_tag,
+            $new_impl_tag
+        );
+
+        // Note: no need to call derive_ffi_traits! because there is a blanket impl for all Arc<T>
+    };
+}
+
+// Generic code between the two macros above
+#[doc(hidden)]
+#[macro_export]
+macro_rules! do_ffi_converter_forward {
+    ($trait:ident, $rust_type:ty, $T:ty, $existing_impl_tag:ty, $new_impl_tag:ty) => {
+        unsafe impl $crate::$trait<$new_impl_tag> for $T {
+            type FfiType = <$T as $crate::$trait<$existing_impl_tag>>::FfiType;
+
+            fn lower(obj: $rust_type) -> Self::FfiType {
+                <$T as $crate::$trait<$existing_impl_tag>>::lower(obj)
+            }
+
+            fn try_lift(v: Self::FfiType) -> $crate::Result<$rust_type> {
+                <$T as $crate::$trait<$existing_impl_tag>>::try_lift(v)
+            }
+
+            fn write(obj: $rust_type, buf: &mut Vec<u8>) {
+                <$T as $crate::$trait<$existing_impl_tag>>::write(obj, buf)
+            }
+
+            fn try_read(buf: &mut &[u8]) -> $crate::Result<$rust_type> {
+                <$T as $crate::$trait<$existing_impl_tag>>::try_read(buf)
+            }
+
+            const TYPE_ID_META: ::uniffi::MetadataBuffer =
+                <$T as $crate::$trait<$existing_impl_tag>>::TYPE_ID_META;
+        }
+    };
+}
+
+#[cfg(test)]
+mod test {
+    use super::{FfiConverter, UniFfiTag};
+    use std::time::{Duration, SystemTime};
+
+    #[test]
+    fn timestamp_roundtrip_post_epoch() {
+        let expected = SystemTime::UNIX_EPOCH + Duration::new(100, 100);
+        let result =
+            <SystemTime as FfiConverter<UniFfiTag>>::try_lift(<SystemTime as FfiConverter<
+                UniFfiTag,
+            >>::lower(expected))
+            .expect("Failed to lift!");
+        assert_eq!(expected, result)
+    }
+
+    #[test]
+    fn timestamp_roundtrip_pre_epoch() {
+        let expected = SystemTime::UNIX_EPOCH - Duration::new(100, 100);
+        let result =
+            <SystemTime as FfiConverter<UniFfiTag>>::try_lift(<SystemTime as FfiConverter<
+                UniFfiTag,
+            >>::lower(expected))
+            .expect("Failed to lift!");
+        assert_eq!(
+            expected, result,
+            "Expected results after lowering and lifting to be equal"
+        )
+    }
+}
+
+#[cfg(test)]
+pub mod test_util {
+    use std::{error::Error, fmt};
+
+    use super::*;
+
+    #[derive(Clone, Debug, PartialEq, Eq)]
+    pub struct TestError(pub String);
+
+    // Use FfiConverter to simplify lifting TestError out of RustBuffer to check it
+    unsafe impl<UT> FfiConverter<UT> for TestError {
+        ffi_converter_rust_buffer_lift_and_lower!(UniFfiTag);
+
+        fn write(obj: TestError, buf: &mut Vec<u8>) {
+            <String as FfiConverter<UniFfiTag>>::write(obj.0, buf);
+        }
+
+        fn try_read(buf: &mut &[u8]) -> Result<TestError> {
+            <String as FfiConverter<UniFfiTag>>::try_read(buf).map(TestError)
+        }
+
+        // Use a dummy value here since we don't actually need TYPE_ID_META
+        const TYPE_ID_META: MetadataBuffer = MetadataBuffer::new();
+    }
+
+    impl fmt::Display for TestError {
+        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+            write!(f, "{}", self.0)
+        }
+    }
+
+    impl Error for TestError {}
+
+    impl<T: Into<String>> From<T> for TestError {
+        fn from(v: T) -> Self {
+            Self(v.into())
+        }
+    }
+
+    derive_ffi_traits!(blanket TestError);
+}
diff --git a/src/metadata.rs b/src/metadata.rs
new file mode 100644
index 0000000..934d09c
--- /dev/null
+++ b/src/metadata.rs
@@ -0,0 +1,277 @@
+/* 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/. */
+
+//! Pack UniFFI interface metadata into byte arrays
+//!
+//! In order to generate foreign bindings, we store interface metadata inside the library file
+//! using exported static byte arrays.  The foreign bindings code reads that metadata from the
+//! library files and generates bindings based on that.
+//!
+//! The metadata static variables are generated by the proc-macros, which is an issue because the
+//! proc-macros don't have knowledge of the entire interface -- they can only see the item they're
+//! wrapping.  For example, when a proc-macro sees a type name, it doesn't know anything about the
+//! actual type: it could be a Record, an Enum, or even a type alias for a `Vec<>`/`Result<>`.
+//!
+//! This module helps bridge the gap by providing tools that allow the proc-macros to generate code
+//! to encode the interface metadata:
+//!   - A set of const functions to build up metadata buffers with const expressions
+//!   - The `export_static_metadata_var!` macro, which creates the static variable from a const metadata
+//!     buffer.
+//!   - The `FfiConverter::TYPE_ID_META` const which encodes an identifier for that type in a
+//!     metadata buffer.
+//!
+//! `uniffi_bindgen::macro_metadata` contains the code to read the metadata from a library file.
+//! `fixtures/metadata` has the tests.
+
+/// Metadata constants, make sure to keep this in sync with copy in `uniffi_meta::reader`
+pub mod codes {
+    // Top-level metadata item codes
+    pub const FUNC: u8 = 0;
+    pub const METHOD: u8 = 1;
+    pub const RECORD: u8 = 2;
+    pub const ENUM: u8 = 3;
+    pub const INTERFACE: u8 = 4;
+    pub const NAMESPACE: u8 = 6;
+    pub const CONSTRUCTOR: u8 = 7;
+    pub const UDL_FILE: u8 = 8;
+    pub const CALLBACK_INTERFACE: u8 = 9;
+    pub const TRAIT_METHOD: u8 = 10;
+    pub const UNIFFI_TRAIT: u8 = 11;
+    pub const TRAIT_INTERFACE: u8 = 12;
+    pub const CALLBACK_TRAIT_INTERFACE: u8 = 13;
+    pub const UNKNOWN: u8 = 255;
+
+    // Type codes
+    pub const TYPE_U8: u8 = 0;
+    pub const TYPE_U16: u8 = 1;
+    pub const TYPE_U32: u8 = 2;
+    pub const TYPE_U64: u8 = 3;
+    pub const TYPE_I8: u8 = 4;
+    pub const TYPE_I16: u8 = 5;
+    pub const TYPE_I32: u8 = 6;
+    pub const TYPE_I64: u8 = 7;
+    pub const TYPE_F32: u8 = 8;
+    pub const TYPE_F64: u8 = 9;
+    pub const TYPE_BOOL: u8 = 10;
+    pub const TYPE_STRING: u8 = 11;
+    pub const TYPE_OPTION: u8 = 12;
+    pub const TYPE_RECORD: u8 = 13;
+    pub const TYPE_ENUM: u8 = 14;
+    // 15 no longer used.
+    pub const TYPE_INTERFACE: u8 = 16;
+    pub const TYPE_VEC: u8 = 17;
+    pub const TYPE_HASH_MAP: u8 = 18;
+    pub const TYPE_SYSTEM_TIME: u8 = 19;
+    pub const TYPE_DURATION: u8 = 20;
+    pub const TYPE_CALLBACK_INTERFACE: u8 = 21;
+    pub const TYPE_CUSTOM: u8 = 22;
+    pub const TYPE_RESULT: u8 = 23;
+    pub const TYPE_TRAIT_INTERFACE: u8 = 24;
+    pub const TYPE_CALLBACK_TRAIT_INTERFACE: u8 = 25;
+    pub const TYPE_UNIT: u8 = 255;
+
+    // Literal codes for LiteralMetadata - note that we don't support
+    // all variants in the "emit/reader" context.
+    pub const LIT_STR: u8 = 0;
+    pub const LIT_INT: u8 = 1;
+    pub const LIT_FLOAT: u8 = 2;
+    pub const LIT_BOOL: u8 = 3;
+    pub const LIT_NULL: u8 = 4;
+}
+
+const BUF_SIZE: usize = 4096;
+
+// This struct is a kludge around the fact that Rust const generic support doesn't quite handle our
+// needs.
+//
+// We'd like to have code like this in `FfiConverter`:
+//
+// ```
+//   const TYPE_ID_META_SIZE: usize;
+//   const TYPE_ID_META: [u8, Self::TYPE_ID_META_SIZE];
+// ```
+//
+// This would define a metadata buffer, correctly size for the data needed. However, associated
+// consts as generic params aren't supported yet.
+//
+// To work around this, we use `const MetadataBuffer` values, which contain fixed-sized buffers
+// with enough capacity to store our largest metadata arrays.  Since the `MetadataBuffer` values
+// are const, they're only stored at compile time and the extra bytes don't end up contributing to
+// the final binary size.  This was tested on Rust `1.66.0` with `--release` by increasing
+// `BUF_SIZE` and checking the compiled library sizes.
+#[derive(Debug)]
+pub struct MetadataBuffer {
+    pub bytes: [u8; BUF_SIZE],
+    pub size: usize,
+}
+
+impl MetadataBuffer {
+    pub const fn new() -> Self {
+        Self {
+            bytes: [0; BUF_SIZE],
+            size: 0,
+        }
+    }
+
+    pub const fn from_code(value: u8) -> Self {
+        Self::new().concat_value(value)
+    }
+
+    // Concatenate another buffer to this one.
+    //
+    // This consumes self, which is convenient for the proc-macro code and also allows us to avoid
+    // allocated an extra buffer.
+    pub const fn concat(mut self, other: MetadataBuffer) -> MetadataBuffer {
+        assert!(self.size + other.size <= BUF_SIZE);
+        // It would be nice to use `copy_from_slice()`, but that's not allowed in const functions
+        // as of Rust 1.66.
+        let mut i = 0;
+        while i < other.size {
+            self.bytes[self.size] = other.bytes[i];
+            self.size += 1;
+            i += 1;
+        }
+        self
+    }
+
+    // Concatenate a `u8` value to this buffer
+    //
+    // This consumes self, which is convenient for the proc-macro code and also allows us to avoid
+    // allocated an extra buffer.
+    pub const fn concat_value(mut self, value: u8) -> Self {
+        assert!(self.size < BUF_SIZE);
+        self.bytes[self.size] = value;
+        self.size += 1;
+        self
+    }
+
+    // Concatenate a `u32` value to this buffer
+    //
+    // This consumes self, which is convenient for the proc-macro code and also allows us to avoid
+    // allocated an extra buffer.
+    pub const fn concat_u32(mut self, value: u32) -> Self {
+        assert!(self.size + 4 <= BUF_SIZE);
+        // store the value as little-endian
+        self.bytes[self.size] = value as u8;
+        self.bytes[self.size + 1] = (value >> 8) as u8;
+        self.bytes[self.size + 2] = (value >> 16) as u8;
+        self.bytes[self.size + 3] = (value >> 24) as u8;
+        self.size += 4;
+        self
+    }
+
+    // Concatenate a `bool` value to this buffer
+    //
+    // This consumes self, which is convenient for the proc-macro code and also allows us to avoid
+    // allocated an extra buffer.
+    pub const fn concat_bool(self, value: bool) -> Self {
+        self.concat_value(value as u8)
+    }
+
+    // Option<bool>
+    pub const fn concat_option_bool(self, value: Option<bool>) -> Self {
+        self.concat_value(match value {
+            None => 0,
+            Some(false) => 1,
+            Some(true) => 2,
+        })
+    }
+
+    // Concatenate a string to this buffer. The maximum string length is 255 bytes. For longer strings,
+    // use `concat_long_str()`.
+    //
+    // Strings are encoded as a `u8` length, followed by the utf8 data.
+    //
+    // This consumes self, which is convenient for the proc-macro code and also allows us to avoid
+    // allocated an extra buffer.
+    pub const fn concat_str(mut self, string: &str) -> Self {
+        assert!(string.len() < 256);
+        assert!(self.size + string.len() < BUF_SIZE);
+        self.bytes[self.size] = string.len() as u8;
+        self.size += 1;
+        let bytes = string.as_bytes();
+        let mut i = 0;
+        while i < bytes.len() {
+            self.bytes[self.size] = bytes[i];
+            self.size += 1;
+            i += 1;
+        }
+        self
+    }
+
+    // Concatenate a longer string to this buffer.
+    //
+    // Strings are encoded as a `u16` length, followed by the utf8 data.
+    //
+    // This consumes self, which is convenient for the proc-macro code and also allows us to avoid
+    // allocated an extra buffer.
+    pub const fn concat_long_str(mut self, string: &str) -> Self {
+        assert!(self.size + string.len() + 1 < BUF_SIZE);
+        let [lo, hi] = (string.len() as u16).to_le_bytes();
+        self.bytes[self.size] = lo;
+        self.bytes[self.size + 1] = hi;
+        self.size += 2;
+        let bytes = string.as_bytes();
+        let mut i = 0;
+        while i < bytes.len() {
+            self.bytes[self.size] = bytes[i];
+            self.size += 1;
+            i += 1;
+        }
+        self
+    }
+
+    // Create an array from this MetadataBuffer
+    //
+    // SIZE should always be `self.size`.  This is part of the kludge to hold us over until Rust
+    // gets better const generic support.
+    pub const fn into_array<const SIZE: usize>(self) -> [u8; SIZE] {
+        let mut result: [u8; SIZE] = [0; SIZE];
+        let mut i = 0;
+        while i < SIZE {
+            result[i] = self.bytes[i];
+            i += 1;
+        }
+        result
+    }
+
+    // Create a checksum from this MetadataBuffer
+    //
+    // This is used by the bindings code to verify that the library they link to is the same one
+    // that the bindings were generated from.
+    pub const fn checksum(&self) -> u16 {
+        calc_checksum(&self.bytes, self.size)
+    }
+}
+
+impl AsRef<[u8]> for MetadataBuffer {
+    fn as_ref(&self) -> &[u8] {
+        &self.bytes[..self.size]
+    }
+}
+
+// Create a checksum for a MetadataBuffer
+//
+// This is used by the bindings code to verify that the library they link to is the same one
+// that the bindings were generated from.
+pub const fn checksum_metadata(buf: &[u8]) -> u16 {
+    calc_checksum(buf, buf.len())
+}
+
+const fn calc_checksum(bytes: &[u8], size: usize) -> u16 {
+    // Taken from the fnv_hash() function from the FNV crate (https://github.com/servo/rust-fnv/blob/master/lib.rs).
+    // fnv_hash() hasn't been released in a version yet.
+    const INITIAL_STATE: u64 = 0xcbf29ce484222325;
+    const PRIME: u64 = 0x100000001b3;
+
+    let mut hash = INITIAL_STATE;
+    let mut i = 0;
+    while i < size {
+        hash ^= bytes[i] as u64;
+        hash = hash.wrapping_mul(PRIME);
+        i += 1;
+    }
+    // Convert the 64-bit hash to a 16-bit hash by XORing everything together
+    (hash ^ (hash >> 16) ^ (hash >> 32) ^ (hash >> 48)) as u16
+}
diff --git a/src/panichook.rs b/src/panichook.rs
new file mode 100644
index 0000000..ef0ab86
--- /dev/null
+++ b/src/panichook.rs
@@ -0,0 +1,34 @@
+/// Initialize our panic handling hook to optionally log panics
+#[cfg(feature = "log_panics")]
+pub fn ensure_setup() {
+    use std::sync::Once;
+    static INIT_BACKTRACES: Once = Once::new();
+    INIT_BACKTRACES.call_once(move || {
+        #[cfg(all(feature = "log_backtraces", not(target_os = "android")))]
+        {
+            std::env::set_var("RUST_BACKTRACE", "1");
+        }
+        // Turn on a panic hook which logs both backtraces and the panic
+        // "Location" (file/line). We do both in case we've been stripped,
+        // ).
+        std::panic::set_hook(Box::new(move |panic_info| {
+            let (file, line) = if let Some(loc) = panic_info.location() {
+                (loc.file(), loc.line())
+            } else {
+                // Apparently this won't happen but rust has reserved the
+                // ability to start returning None from location in some cases
+                // in the future.
+                ("<unknown>", 0)
+            };
+            log::error!("### Rust `panic!` hit at file '{file}', line {line}");
+            #[cfg(all(feature = "log_backtraces", not(target_os = "android")))]
+            {
+                log::error!("  Complete stack trace:\n{:?}", backtrace::Backtrace::new());
+            }
+        }));
+    });
+}
+
+/// Initialize our panic handling hook to optionally log panics
+#[cfg(not(feature = "log_panics"))]
+pub fn ensure_setup() {}