From 697819ec38e84b759078f1b388090b6b1bcdaddc Mon Sep 17 00:00:00 2001 From: Ben Dean-Kawamura Date: Thu, 12 Oct 2023 20:22:43 -0400 Subject: [PATCH] Implemented slab storage that allocates handles (#1730) This will be used for passing handles across the FFI. Use proptest to test the Rust implementation. Created a new fixture to test foreign bindings code for this. The docs list list of advantages to this, the main disadvantage is performance. We need to allocate a vec entry for the pointer and insert/remove/get need to take a lock. Compared `Arc` this is clearly worse. However, compared to `Arc` it's not so clear when we currently box the arc before passing it across the FFI. Compared to the foreign HandleMaps, I think the new system will be faster. It seems reasonable to trade some performance for the gains in simplicity and safety. The one thing that really bugs me is that `get()` needs to take a read lock. I'm pretty sure there's a couple ways to fix this, but I'm not sure if it's worth the complexity. --- Cargo.lock | 225 ++++++++- Cargo.toml | 1 + fixtures/bindings-internal/Cargo.toml | 20 + .../src/bindings_internal.udl | 1 + fixtures/bindings-internal/src/lib.rs | 7 + .../tests/bindings/test_bindings_internal.kts | 35 ++ .../tests/bindings/test_bindings_internal.py | 32 ++ .../tests/bindings/test_bindings_internal.rb | 46 ++ .../bindings/test_bindings_internal.swift | 34 ++ .../tests/test_generated_bindings.rs | 6 + fixtures/bindings-internal/uniffi.toml | 5 + .../src/bindings/kotlin/gen_kotlin/mod.rs | 10 + .../templates/CallbackInterfaceTemplate.kt | 2 - .../src/bindings/kotlin/templates/Helpers.kt | 2 +- .../src/bindings/kotlin/templates/Slab.kt | 127 +++++ .../src/bindings/kotlin/templates/wrapper.kt | 5 + .../templates/CallbackInterfaceRuntime.py | 2 - .../src/bindings/python/templates/Slab.py | 91 ++++ .../src/bindings/python/templates/wrapper.py | 3 + .../src/bindings/ruby/templates/Slab.rb | 102 ++++ .../src/bindings/ruby/templates/wrapper.rb | 1 + .../src/bindings/swift/gen_swift/mod.rs | 10 + .../templates/CallbackInterfaceRuntime.swift | 8 - .../bindings/swift/templates/Helpers.swift | 15 +- .../src/bindings/swift/templates/Slab.swift | 121 +++++ .../bindings/swift/templates/wrapper.swift | 1 + uniffi_core/Cargo.toml | 4 + uniffi_core/src/ffi/ffidefault.rs | 6 + uniffi_core/src/ffi/mod.rs | 2 + uniffi_core/src/ffi/slab.rs | 462 ++++++++++++++++++ 30 files changed, 1369 insertions(+), 17 deletions(-) create mode 100644 fixtures/bindings-internal/Cargo.toml create mode 100644 fixtures/bindings-internal/src/bindings_internal.udl create mode 100644 fixtures/bindings-internal/src/lib.rs create mode 100644 fixtures/bindings-internal/tests/bindings/test_bindings_internal.kts create mode 100644 fixtures/bindings-internal/tests/bindings/test_bindings_internal.py create mode 100644 fixtures/bindings-internal/tests/bindings/test_bindings_internal.rb create mode 100644 fixtures/bindings-internal/tests/bindings/test_bindings_internal.swift create mode 100644 fixtures/bindings-internal/tests/test_generated_bindings.rs create mode 100644 fixtures/bindings-internal/uniffi.toml create mode 100644 uniffi_bindgen/src/bindings/kotlin/templates/Slab.kt create mode 100644 uniffi_bindgen/src/bindings/python/templates/Slab.py create mode 100644 uniffi_bindgen/src/bindings/ruby/templates/Slab.rb create mode 100644 uniffi_bindgen/src/bindings/swift/templates/Slab.swift create mode 100644 uniffi_core/src/ffi/slab.rs diff --git a/Cargo.lock b/Cargo.lock index ef1d6bfe1b..d547ad3400 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -158,7 +158,7 @@ dependencies = [ "async-lock", "async-task", "concurrent-queue", - "fastrand", + "fastrand 1.9.0", "futures-lite", "slab", ] @@ -284,6 +284,21 @@ dependencies = [ "serde", ] +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitflags" version = "1.3.2" @@ -306,7 +321,7 @@ dependencies = [ "async-lock", "async-task", "atomic-waker", - "fastrand", + "fastrand 1.9.0", "futures-lite", "log", ] @@ -470,6 +485,28 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "const-random" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368a7a772ead6ce7e1de82bfb04c485f3db8ec744f72925af5735e29a22cc18e" +dependencies = [ + "const-random-macro", + "proc-macro-hack", +] + +[[package]] +name = "const-random-macro" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d7d6ab3c3a2282db210df5f02c4dab6e0a7057af0fb7ebd4070f30fe05c0ddb" +dependencies = [ + "getrandom", + "once_cell", + "proc-macro-hack", + "tiny-keccak", +] + [[package]] name = "criterion" version = "0.5.1" @@ -549,6 +586,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "either" version = "1.9.0" @@ -591,6 +634,12 @@ dependencies = [ "instant", ] +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + [[package]] name = "fnv" version = "1.0.7" @@ -639,7 +688,7 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" dependencies = [ - "fastrand", + "fastrand 1.9.0", "futures-core", "futures-io", "memchr", @@ -661,6 +710,17 @@ dependencies = [ "windows", ] +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "gimli" version = "0.28.0" @@ -829,6 +889,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + [[package]] name = "linux-raw-sys" version = "0.3.8" @@ -946,6 +1012,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -1074,6 +1141,18 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + [[package]] name = "proc-macro2" version = "1.0.66" @@ -1083,6 +1162,32 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proptest" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c003ac8c77cb07bb74f5f198bce836a689bcd5a42574612bf14d17bfd08c20e" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags 2.4.0", + "lazy_static", + "num-traits", + "rand", + "rand_chacha", + "rand_xorshift", + "regex-syntax 0.7.4", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quote" version = "1.0.33" @@ -1092,6 +1197,45 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core", +] + [[package]] name = "rayon" version = "1.7.0" @@ -1114,6 +1258,15 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "regex" version = "1.9.3" @@ -1197,6 +1350,18 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" +[[package]] +name = "rusty-fork" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + [[package]] name = "ryu" version = "1.0.15" @@ -1357,6 +1522,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" +dependencies = [ + "cfg-if", + "fastrand 2.0.1", + "redox_syscall", + "rustix 0.38.8", + "windows-sys", +] + [[package]] name = "termcolor" version = "1.2.0" @@ -1396,6 +1574,15 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinytemplate" version = "1.2.1" @@ -1517,6 +1704,12 @@ dependencies = [ "termcolor", ] +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unary-result-alias" version = "0.1.0" @@ -1659,6 +1852,15 @@ dependencies = [ "uniffi_bindgen", ] +[[package]] +name = "uniffi-fixture-bindings-internal" +version = "0.1.0" +dependencies = [ + "proptest", + "thiserror", + "uniffi", +] + [[package]] name = "uniffi-fixture-callbacks" version = "0.22.0" @@ -1980,10 +2182,12 @@ dependencies = [ "async-compat", "bytes", "camino", + "const-random", "log", "once_cell", "oneshot", "paste", + "rand", "static_assertions", ] @@ -2080,6 +2284,15 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + [[package]] name = "waker-fn" version = "1.1.0" @@ -2096,6 +2309,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + [[package]] name = "wasm-bindgen" version = "0.2.87" diff --git a/Cargo.toml b/Cargo.toml index 4d919af4a0..24345741b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ members = [ "examples/traits", "fixtures/benchmarks", + "fixtures/bindings-internal", "fixtures/coverall", "fixtures/callbacks", diff --git a/fixtures/bindings-internal/Cargo.toml b/fixtures/bindings-internal/Cargo.toml new file mode 100644 index 0000000000..b7f81988ca --- /dev/null +++ b/fixtures/bindings-internal/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "uniffi-fixture-bindings-internal" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["lib", "cdylib"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +proptest = "1.0" +thiserror = "1" +uniffi = {path = "../../uniffi", version = "0.24" } + +[build-dependencies] +uniffi = {path = "../../uniffi", version = "0.24", features = ["build"] } + +[dev-dependencies] +uniffi = {path = "../../uniffi", version = "0.24", features = ["bindgen-tests"] } diff --git a/fixtures/bindings-internal/src/bindings_internal.udl b/fixtures/bindings-internal/src/bindings_internal.udl new file mode 100644 index 0000000000..3a7c54a37b --- /dev/null +++ b/fixtures/bindings-internal/src/bindings_internal.udl @@ -0,0 +1 @@ +namespace bindings_internal { }; diff --git a/fixtures/bindings-internal/src/lib.rs b/fixtures/bindings-internal/src/lib.rs new file mode 100644 index 0000000000..5ee84c2adc --- /dev/null +++ b/fixtures/bindings-internal/src/lib.rs @@ -0,0 +1,7 @@ +/* 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 crate exists to test the internal Uniffi functionality of the bindings code + +uniffi::setup_scaffolding!("bindings_internal"); diff --git a/fixtures/bindings-internal/tests/bindings/test_bindings_internal.kts b/fixtures/bindings-internal/tests/bindings/test_bindings_internal.kts new file mode 100644 index 0000000000..64c1dd8815 --- /dev/null +++ b/fixtures/bindings-internal/tests/bindings/test_bindings_internal.kts @@ -0,0 +1,35 @@ +/* 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/. */ + +import uniffi.bindings_internal.InternalException +import uniffi.bindings_internal.UniffiSlab + +val slab = UniffiSlab(0) +val handle1 = slab.insert(0) +val handle2 = slab.insert(1) +assert(slab.get(handle1) == 0) +assert(slab.get(handle2) == 1) +slab.remove(handle1) + +try { + slab.get(handle1) + throw AssertionError("get with a removed handle should fail") +} catch (_: InternalException) { + // Expected +} + +val slab2 = UniffiSlab(1) +try { + slab2.get(handle2) + throw AssertionError("get with a handle from a different slab should fail") +} catch (_: InternalException) { + // Expected +} + +try { + slab.get(handle2 and 0x0001_0000_0000.inv()) + throw AssertionError("get with a handle from a Rust slab should fail") +} catch (_: InternalException) { + // Expected +} diff --git a/fixtures/bindings-internal/tests/bindings/test_bindings_internal.py b/fixtures/bindings-internal/tests/bindings/test_bindings_internal.py new file mode 100644 index 0000000000..97c1370488 --- /dev/null +++ b/fixtures/bindings-internal/tests/bindings/test_bindings_internal.py @@ -0,0 +1,32 @@ +# 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/. */ + +import contextlib +import unittest +from bindings_internal import * +from bindings_internal import UniffiSlab + +class InternalsTest(unittest.TestCase): + def test_slab(self): + slab = UniffiSlab(0) + handle1 = slab.insert(0) + handle2 = slab.insert(1) + self.assertEqual(slab.get(handle1), 0) + self.assertEqual(slab.get(handle2), 1) + slab.remove(handle1) + # Re-using a removed handle should fail + with self.assertRaises(InternalError): + slab.get(handle1) + + # Using a handle with a different slab should fail + slab2 = UniffiSlab(1) + with self.assertRaises(InternalError): + slab2.get(handle2) + + # Using a handle from rust should fail + with self.assertRaises(InternalError): + slab.get(handle2 & ~UniffiSlab.FOREIGN_BIT) + +if __name__=='__main__': + unittest.main() diff --git a/fixtures/bindings-internal/tests/bindings/test_bindings_internal.rb b/fixtures/bindings-internal/tests/bindings/test_bindings_internal.rb new file mode 100644 index 0000000000..ca770f942b --- /dev/null +++ b/fixtures/bindings-internal/tests/bindings/test_bindings_internal.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +# 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/. */ + +require 'test/unit' +require 'bindings_internal' + +class TestSlab < Test::Unit::TestCase + def test_slaB + slab = BindingsInternal::UniffiSlab.new(0) + handle1 = slab.insert(0) + handle2 = slab.insert(1) + assert_equal(slab.get(handle1), 0) + assert_equal(slab.get(handle2), 1) + slab.remove(handle1) + # Re-using a removed handle should fail + begin + slab.get(handle1) + rescue BindingsInternal::InternalError => err + # Expected + else + raise 'should have thrown' + end + + # Using a handle with a different slab should fail + slab2 = BindingsInternal::UniffiSlab.new(1) + begin + slab2.get(handle2) + rescue BindingsInternal::InternalError => err + # Expected + else + raise 'should have thrown' + end + + # Using a handle from Rust should fail + begin + slab.get(handle2 & ~0x0001_0000_0000) + rescue BindingsInternal::InternalError => err + # Expected + else + raise 'should have thrown' + end + end +end diff --git a/fixtures/bindings-internal/tests/bindings/test_bindings_internal.swift b/fixtures/bindings-internal/tests/bindings/test_bindings_internal.swift new file mode 100644 index 0000000000..1767ff1602 --- /dev/null +++ b/fixtures/bindings-internal/tests/bindings/test_bindings_internal.swift @@ -0,0 +1,34 @@ +// 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/. + +import bindings_internal + +var slab = UniffiSlab(0) +let handle1 = try! slab.insert(0) +let handle2 = try! slab.insert(1) +assert(try! slab.get(handle1) == 0) +assert(try! slab.get(handle2) == 1) +let _ = try! slab.remove(handle1) + +do { + let _ = try slab.get(handle1) + fatalError("get with a removed handle should fail") +} catch UniffiInternalError.slabUseAfterFree { + // Expected +} + +var slab2 = UniffiSlab(1) +do { + let _ = try slab2.get(handle2) + fatalError("get with a handle from a different slab should fail") +} catch UniffiInternalError.slabError { + // Expected +} + +do { + let _ = try slab.get(handle2 & ~0x0001_0000_0000) + fatalError("get with a handle from a Rust slab should fail") +} catch UniffiInternalError.slabError { + // Expected +} diff --git a/fixtures/bindings-internal/tests/test_generated_bindings.rs b/fixtures/bindings-internal/tests/test_generated_bindings.rs new file mode 100644 index 0000000000..8935022392 --- /dev/null +++ b/fixtures/bindings-internal/tests/test_generated_bindings.rs @@ -0,0 +1,6 @@ +uniffi::build_foreign_language_testcases!( + "tests/bindings/test_bindings_internal.py", + "tests/bindings/test_bindings_internal.kts", + "tests/bindings/test_bindings_internal.rb", + "tests/bindings/test_bindings_internal.swift", +); diff --git a/fixtures/bindings-internal/uniffi.toml b/fixtures/bindings-internal/uniffi.toml new file mode 100644 index 0000000000..ec2a7688c6 --- /dev/null +++ b/fixtures/bindings-internal/uniffi.toml @@ -0,0 +1,5 @@ +[bindings.kotlin] +test_mode = true + +[bindings.swift] +test_mode = true diff --git a/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs b/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs index 2461b590d3..49aef3a06b 100644 --- a/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs +++ b/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs @@ -37,6 +37,8 @@ pub struct Config { custom_types: HashMap, #[serde(default)] external_packages: HashMap, + #[serde(default)] + test_mode: bool, } #[derive(Debug, Default, Clone, Serialize, Deserialize)] @@ -197,6 +199,7 @@ pub struct KotlinWrapper<'a> { type_helper_code: String, type_imports: BTreeSet, has_async_fns: bool, + internal_component_vis: String, } impl<'a> KotlinWrapper<'a> { @@ -204,12 +207,19 @@ impl<'a> KotlinWrapper<'a> { let type_renderer = TypeRenderer::new(&config, ci); let type_helper_code = type_renderer.render().unwrap(); let type_imports = type_renderer.imports.into_inner(); + let internal_component_vis = if config.test_mode { + "public" + } else { + "internal" + } + .to_owned(); Self { config, ci, type_helper_code, type_imports, has_async_fns: ci.has_async_fns(), + internal_component_vis, } } diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceTemplate.kt b/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceTemplate.kt index 56ae558544..9adb7f20c8 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceTemplate.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceTemplate.kt @@ -4,8 +4,6 @@ {% if self.include_once_check("CallbackInterfaceRuntime.kt") %}{% include "CallbackInterfaceRuntime.kt" %}{% endif %} {{- self.add_import("java.util.concurrent.atomic.AtomicLong") }} -{{- self.add_import("java.util.concurrent.locks.ReentrantLock") }} -{{- self.add_import("kotlin.concurrent.withLock") }} // Declaration and FfiConverters for {{ type_name }} Callback Interface diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/Helpers.kt b/uniffi_bindgen/src/bindings/kotlin/templates/Helpers.kt index 382a5f7413..fae13280c1 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/Helpers.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/Helpers.kt @@ -21,7 +21,7 @@ internal open class RustCallStatus : Structure() { } } -class InternalException(message: String) : Exception(message) +open class InternalException(message: String) : Exception(message) // Each top-level error class has a companion object that can lift the error from the call status's rust buffer interface CallStatusErrorHandler { diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/Slab.kt b/uniffi_bindgen/src/bindings/kotlin/templates/Slab.kt new file mode 100644 index 0000000000..5dd1c2aa88 --- /dev/null +++ b/uniffi_bindgen/src/bindings/kotlin/templates/Slab.kt @@ -0,0 +1,127 @@ +// Slab and handle handling. +// +// This is a copy of `uniffi_core/src/ffi/slab.rs`. See that module for documentation. + +// Note: Handles are u64 values, but the top 16 bits are unset, so we can treat them as signed +// without issue. The JNA integration is easier if they're signed, so let's go with that. +internal typealias UniffiKotlinHandle = Long +class UniffiSlabUseAfterFreeException(message: String) : InternalException(message) { } + +{{ internal_component_vis }} class UniffiSlab(slabId: Long? = null) { + private val slabId = if (slabId == null) { + nextSlabId() + } else { + ((slabId * SLAB_ID_UNIT) and SLAB_ID_MASK) or FOREIGN_BIT + } + private var generation = 0L + private var next = END_OF_LIST + private val lock = ReentrantReadWriteLock() + private val entries = ArrayList>() + + sealed class Entry { + data class Vacant(val next: Int) : Entry() + data class Occupied(val generation: Long, val value: T) : Entry() + } + + companion object { + internal var idCounter: Long = 0 + + private const val MAX_ENTRIES: Long = 4_000_000_000; + private const val END_OF_LIST: Int = Int.MAX_VALUE + private const val INDEX_MASK: Long = 0x0000_FFFF_FFFF; + private const val SLAB_ID_MASK: Long = 0x00FF_0000_0000; + private const val SLAB_ID_UNIT: Long = 0x0002_0000_0000; + private const val FOREIGN_BIT: Long = 0x0001_0000_0000; + private const val GENERATION_MASK: Long = 0xFF00_0000_0000; + private const val GENERATION_UNIT: Long = 0x0100_0000_0000; + + fun nextSlabId(): Long { + idCounter = (idCounter + SLAB_ID_UNIT) and SLAB_ID_MASK + return idCounter or FOREIGN_BIT + } + } + + private fun lookup(handle: UniffiKotlinHandle): Int { + val index = (handle and INDEX_MASK).toInt() + val handleSlabId = handle and SLAB_ID_MASK + val handleGeneration = handle and GENERATION_MASK + + if (handleSlabId != this.slabId) { + if (handleSlabId and FOREIGN_BIT == 0L) { + throw InternalException("Handle belongs to a Rust slab") + } else { + throw InternalException("Slab id mismatch") + } + } + val entry = entries[index] + when (entry) { + is Entry.Vacant -> throw UniffiSlabUseAfterFreeException("entry vacant") + is Entry.Occupied -> { + if (entry.generation != handleGeneration) { + throw UniffiSlabUseAfterFreeException("generation mismatch") + } + return index + } + } + } + + private fun makeHandle(generation: Long, index: Int): UniffiKotlinHandle { + return index.toLong() or generation or slabId + } + + private fun getVacantToUse(): Int { + if (next == END_OF_LIST) { + entries.add(Entry.Vacant(END_OF_LIST)) + return entries.size - 1 + } else { + val oldNext = next + val entry = entries[next] + return when (entry) { + is Entry.Occupied -> throw InternalException("next is occupied") + is Entry.Vacant -> { + next = entry.next + oldNext + } + } + } + } + + {{ internal_component_vis }} fun insert(value: T): UniffiKotlinHandle { + return lock.writeLock().withLock { + val index = getVacantToUse() + generation = (generation + GENERATION_UNIT) and GENERATION_MASK; + entries[index] = Entry.Occupied(generation, value) + makeHandle(generation, index) + } + } + + {{ internal_component_vis }} fun remove(handle: UniffiKotlinHandle): T { + return lock.writeLock().withLock { + val index = lookup(handle) + val entry = entries[index] + when (entry) { + is Entry.Vacant -> throw InternalException("Lookup gave us a vacant entry") + is Entry.Occupied -> { + entries[index] = Entry.Vacant(next) + next = index + entry.value + } + } + } + } + + {{ internal_component_vis }} fun get(handle: UniffiKotlinHandle): T { + return lock.readLock().withLock { + val index = lookup(handle) + val entry = entries[index] + when (entry) { + is Entry.Vacant -> throw InternalException("Lookup gave us a vacant entry") + is Entry.Occupied -> entry.value + } + } + } + + {{ internal_component_vis }} fun specialValue(value: Int): UniffiKotlinHandle { + return MAX_ENTRIES.toLong() + value.toLong() + } +} diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/wrapper.kt b/uniffi_bindgen/src/bindings/kotlin/templates/wrapper.kt index 9ee4229018..d27a95d13a 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/wrapper.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/wrapper.kt @@ -28,7 +28,11 @@ import java.nio.ByteBuffer import java.nio.ByteOrder import java.nio.CharBuffer import java.nio.charset.CodingErrorAction +import java.util.ArrayList import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.locks.ReentrantLock +import java.util.concurrent.locks.ReentrantReadWriteLock +import kotlin.concurrent.withLock {%- for req in self.imports() %} {{ req.render() }} @@ -37,6 +41,7 @@ import java.util.concurrent.ConcurrentHashMap {% include "RustBufferTemplate.kt" %} {% include "FfiConverterTemplate.kt" %} {% include "Helpers.kt" %} +{% include "Slab.kt" %} // Contains loading, initialization code, // and the FFI Function declarations in a com.sun.jna.Library. diff --git a/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceRuntime.py b/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceRuntime.py index 0fe2ab8dc0..c71d509c86 100644 --- a/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceRuntime.py +++ b/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceRuntime.py @@ -1,5 +1,3 @@ -import threading - class ConcurrentHandleMap: """ A map where inserting, getting and removing data is synchronized with a lock. diff --git a/uniffi_bindgen/src/bindings/python/templates/Slab.py b/uniffi_bindgen/src/bindings/python/templates/Slab.py new file mode 100644 index 0000000000..af14ef12de --- /dev/null +++ b/uniffi_bindgen/src/bindings/python/templates/Slab.py @@ -0,0 +1,91 @@ +class UniffiSlab: + MAX_ENTRIES = 4_000_000_000 + END_OF_LIST = sys.maxsize + INDEX_MASK = 0x0000_FFFF_FFFF + SLAB_ID_MASK = 0x00FF_0000_0000 + SLAB_ID_UNIT = 0x0002_0000_0000 + FOREIGN_BIT = 0x0001_0000_0000 + GENERATION_MASK = 0xFF00_0000_0000 + GENERATION_UNIT = 0x0100_0000_0000 + + class UseAfterFree(InternalError): + pass + + @dataclass + class Vacant: + next: int + + @dataclass + class Occupied: + generation: int + value: object + + _slab_id_counter = 0 + + def __init__(self, slab_id=None): + if slab_id is None: + self._slab_id_counter = (self._slab_id_counter + self.SLAB_ID_UNIT) & self.SLAB_ID_MASK + self._slab_id = self._slab_id_counter | self.FOREIGN_BIT + else: + self._slab_id = (slab_id * self.SLAB_ID_UNIT) & self.SLAB_ID_MASK | self.FOREIGN_BIT + self._next = self.END_OF_LIST + self._generation = 0 + self._entries = [] + # Python doesn't support a Rwlock, so we just use a regular lock instead. + self._lock = threading.Lock() + + def _lookup(self, handle: int) -> int: + index = handle & self.INDEX_MASK + handle_slab_id = handle & self.SLAB_ID_MASK + handle_generation = handle & self.GENERATION_MASK + + if handle_slab_id != self._slab_id: + if (handle_slab_id & self.FOREIGN_BIT) == 0: + raise InternalError("Handle belongs to a Rust Slab") + else: + raise InternalError("Invalid slab id") + + entry = self._entries[index] + if not isinstance(entry, self.Occupied): + raise self.UseAfterFree("entry vacant") + if entry.generation != handle_generation: + raise self.UseAfterFree("generation mismatch") + return index + + def _make_handle(self, generation: int, index: int) -> int: + return index | generation | self._slab_id + + def _get_vacant_to_use(self) -> int: + if self._next == self.END_OF_LIST: + self._entries.append(self.Vacant(self.END_OF_LIST)) + return len(self._entries) - 1 + else: + old_next = self._next + next_entry = self._entries[old_next] + if not isinstance(next_entry, self.Vacant): + raise InternalError("self._next is not Vacant") + self._next = next_entry.next + return old_next + + def insert(self, value: object) -> int: + with self._lock: + idx = self._get_vacant_to_use() + self._generation = (self._generation + self.GENERATION_UNIT) & self.GENERATION_MASK + self._entries[idx] = self.Occupied(self._generation, value) + return self._make_handle(self._generation, idx) + + def remove(self, handle: int) -> object: + with self._lock: + idx = self._lookup(handle) + entry = self._entries[idx] + self._entries[idx] = self.Vacant(self._next) + self._next = idx + return entry.value + + def get(self, handle: int) -> object: + with self._lock: + idx = self._lookup(handle) + return self._entries[idx].value + + def special_value(self, val: int) -> int: + return self.MAX_ENTRIES + val diff --git a/uniffi_bindgen/src/bindings/python/templates/wrapper.py b/uniffi_bindgen/src/bindings/python/templates/wrapper.py index 24c3290ff7..9f1da3dc65 100644 --- a/uniffi_bindgen/src/bindings/python/templates/wrapper.py +++ b/uniffi_bindgen/src/bindings/python/templates/wrapper.py @@ -21,6 +21,7 @@ import contextlib import datetime import typing +import threading {%- if ci.has_async_fns() %} import asyncio {%- endif %} @@ -28,12 +29,14 @@ {%- for req in self.imports() %} {{ req.render() }} {%- endfor %} +from dataclasses import dataclass # Used for default argument values _DEFAULT = object() {% include "RustBufferTemplate.py" %} {% include "Helpers.py" %} +{% include "Slab.py" %} {% include "PointerManager.py" %} {% include "RustBufferHelper.py" %} diff --git a/uniffi_bindgen/src/bindings/ruby/templates/Slab.rb b/uniffi_bindgen/src/bindings/ruby/templates/Slab.rb new file mode 100644 index 0000000000..8c249c011d --- /dev/null +++ b/uniffi_bindgen/src/bindings/ruby/templates/Slab.rb @@ -0,0 +1,102 @@ +class UniffiSlab + MAX_ENTRIES = 4_000_000_000 + # Ruby doesn't supply something like Int.MAX, but this is larger than any possible index + END_OF_LIST = 2 ** 32 + INDEX_MASK = 0x0000_FFFF_FFFF + SLAB_ID_MASK = 0x00FF_0000_0000 + SLAB_ID_UNIT = 0x0002_0000_0000 + FOREIGN_BIT = 0x0001_0000_0000 + GENERATION_MASK = 0xFF00_0000_0000 + GENERATION_UNIT = 0x0100_0000_0000 + + Vacant = Data.define(:next) + Occupied = Data.define(:generation, :value) + + @@slab_id_counter = 0 + + def initialize(slab_id=nil) + if slab_id.nil? + @@slab_id_counter = (@@slab_id_counter + SLAB_ID_UNIT) & SLAB_ID_MASK + @slab_id = @@slab_id_counter | FOREIGN_BIT + else + @slab_id = ((slab_id * SLAB_ID_UNIT) & SLAB_ID_MASK) | FOREIGN_BIT + end + @next = END_OF_LIST + @generation = 0 + @entries = [] + # Ruby doesn't provide a standard RwLock, so we use a Mutex instead. + @lock = Mutex.new + end + + private def lookup(handle) + index = handle & INDEX_MASK + handle_slab_id = handle & SLAB_ID_MASK + handle_generation = handle & GENERATION_MASK + + if handle_slab_id != @slab_id + if handle_slab_id & FOREIGN_BIT == 0 + raise InternalError, "Handle belongs to Rust slab" + else + raise InternalError, "Invalid slab id" + end + end + + entry = @entries[index] + if not entry.is_a? Occupied + raise InternalError, "use-after-free (entry vacant)" + end + if entry.generation != handle_generation + raise InternalError, "use-after-free (generation mismatch)" + end + return index + end + + private def make_handle(generation, index) + return index | generation | @slab_id + end + + private def get_vacant_to_use() + if @next == END_OF_LIST + @entries.append(Vacant.new(END_OF_LIST)) + return @entries.length - 1 + else + old_next = @next + next_entry = @entries[old_next] + if not isinstance(next_entry, Vacant) + raise InternalError("next is not Vacant") + end + @next = next_entry.next + return old_next + end + end + + def insert(value) + @lock.synchronize do + idx = get_vacant_to_use() + @generation = (@generation + GENERATION_UNIT) & GENERATION_MASK + @entries[idx] = Occupied.new(@generation, value) + return make_handle(@generation, idx) + end + end + + def remove(handle) + @lock.synchronize do + idx = lookup(handle) + entry = @entries[idx] + @entries[idx] = Vacant.new(@next) + @next = idx + return entry.value + end + end + + def get(handle) + @lock.synchronize do + idx = lookup(handle) + return @entries[idx].value + end + end + + def special_value(val) + return MAX_ENTRIES + val + end +end diff --git a/uniffi_bindgen/src/bindings/ruby/templates/wrapper.rb b/uniffi_bindgen/src/bindings/ruby/templates/wrapper.rb index e3631b68de..5fc5a0bc1d 100644 --- a/uniffi_bindgen/src/bindings/ruby/templates/wrapper.rb +++ b/uniffi_bindgen/src/bindings/ruby/templates/wrapper.rb @@ -18,6 +18,7 @@ module {{ ci.namespace()|class_name_rb }} {% include "Helpers.rb" %} + {% include "Slab.rb" %} {% include "RustBufferTemplate.rb" %} {% include "RustBufferStream.rb" %} diff --git a/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs b/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs index ec38ec11c8..b27d0ee6ec 100644 --- a/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs +++ b/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs @@ -138,6 +138,8 @@ pub struct Config { omit_argument_labels: Option, #[serde(default)] custom_types: HashMap, + #[serde(default)] + test_mode: bool, } #[derive(Debug, Default, Clone, Serialize, Deserialize)] @@ -341,18 +343,26 @@ pub struct SwiftWrapper<'a> { type_helper_code: String, type_imports: BTreeSet, has_async_fns: bool, + internal_component_vis: String, } impl<'a> SwiftWrapper<'a> { pub fn new(config: Config, ci: &'a ComponentInterface) -> Self { let type_renderer = TypeRenderer::new(&config, ci); let type_helper_code = type_renderer.render().unwrap(); let type_imports = type_renderer.imports.into_inner(); + let internal_component_vis = if config.test_mode { + "public" + } else { + "fileprivate" + } + .to_owned(); Self { config, ci, type_helper_code, type_imports, has_async_fns: ci.has_async_fns(), + internal_component_vis, } } diff --git a/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceRuntime.swift b/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceRuntime.swift index 9ae62d1667..59f1a64534 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceRuntime.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceRuntime.swift @@ -1,11 +1,3 @@ -fileprivate extension NSLock { - func withLock(f: () throws -> T) rethrows -> T { - self.lock() - defer { self.unlock() } - return try f() - } -} - fileprivate typealias UniFFICallbackHandle = UInt64 fileprivate class UniFFICallbackHandleMap { private var leftMap: [UniFFICallbackHandle: T] = [:] diff --git a/uniffi_bindgen/src/bindings/swift/templates/Helpers.swift b/uniffi_bindgen/src/bindings/swift/templates/Helpers.swift index a34b128e23..ecf1d9511d 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/Helpers.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/Helpers.swift @@ -1,6 +1,7 @@ // An error type for FFI errors. These errors occur at the UniFFI level, not // the library level. -fileprivate enum UniffiInternalError: LocalizedError { +// public so we can test it in fixtures-bindings-internal +{{ internal_component_vis }} enum UniffiInternalError: LocalizedError { case bufferOverflow case incompleteData case unexpectedOptionalTag @@ -9,6 +10,8 @@ fileprivate enum UniffiInternalError: LocalizedError { case unexpectedRustCallStatusCode case unexpectedRustCallError case unexpectedStaleHandle + case slabUseAfterFree(_ message: String) + case slabError(_ message: String) case rustPanic(_ message: String) public var errorDescription: String? { @@ -21,11 +24,21 @@ fileprivate enum UniffiInternalError: LocalizedError { case .unexpectedRustCallStatusCode: return "Unexpected RustCallStatus code" case .unexpectedRustCallError: return "CALL_ERROR but no errorClass specified" case .unexpectedStaleHandle: return "The object in the handle map has been dropped already" + case let .slabUseAfterFree(message): return "Slab use-after-free: \(message)" + case let .slabError(message): return "Slab error: \(message)" case let .rustPanic(message): return message } } } +fileprivate extension NSLock { + func withLock(f: () throws -> T) rethrows -> T { + self.lock() + defer { self.unlock() } + return try f() + } +} + fileprivate let CALL_SUCCESS: Int8 = 0 fileprivate let CALL_ERROR: Int8 = 1 fileprivate let CALL_PANIC: Int8 = 2 diff --git a/uniffi_bindgen/src/bindings/swift/templates/Slab.swift b/uniffi_bindgen/src/bindings/swift/templates/Slab.swift new file mode 100644 index 0000000000..d867e2359b --- /dev/null +++ b/uniffi_bindgen/src/bindings/swift/templates/Slab.swift @@ -0,0 +1,121 @@ +let UNIFFI_SLAB_MAX_ENTRIES: UInt64 = 4_000_000_000 +let UNIFFI_SLAB_END_OF_LIST: Int = Int.max +let UNIFFI_SLAB_INDEX_MASK: UInt64 = 0x0000_FFFF_FFFF +let UNIFFI_SLAB_SLAB_ID_MASK: UInt64 = 0x00FF_0000_0000 +let UNIFFI_SLAB_SLAB_ID_UNIT: UInt64 = 0x0002_0000_0000 +let UNIFFI_SLAB_FOREIGN_BIT: UInt64 = 0x0001_0000_0000 +let UNIFFI_SLAB_GENERATION_MASK: UInt64 = 0xFF00_0000_0000 +let UNIFFI_SLAB_GENERATION_UNIT: UInt64 = 0x0100_0000_0000 + +private var uniffiSlabIdCounter: UInt64 = 0 + +{{ internal_component_vis }} struct UniffiSlab { + enum Entry { + case vacant(next: Int) + case occupied(generation: UInt64, value: T) + } + + private let slabId: UInt64 + // Should we use `pthread_rwlock_t` instead? + private let lock = NSLock() + private var next: Int = UNIFFI_SLAB_END_OF_LIST + private var generation: UInt64 = 0 + private var entries: [Entry] = [] + + {{ internal_component_vis }} init() { + uniffiSlabIdCounter = (uniffiSlabIdCounter + UNIFFI_SLAB_SLAB_ID_UNIT) & UNIFFI_SLAB_SLAB_ID_MASK + self.slabId = uniffiSlabIdCounter | UNIFFI_SLAB_FOREIGN_BIT + } + + {{ internal_component_vis }} init(_ slabId: Int) { + self.slabId = ((UInt64(slabId) * UNIFFI_SLAB_SLAB_ID_UNIT) & UNIFFI_SLAB_SLAB_ID_MASK) | UNIFFI_SLAB_FOREIGN_BIT + } + + private func lookup(handle: UInt64) throws -> Int { + let index = Int(handle & UNIFFI_SLAB_INDEX_MASK) + let handleSlabId = handle & UNIFFI_SLAB_SLAB_ID_MASK + let handleGeneration = handle & UNIFFI_SLAB_GENERATION_MASK + + if handleSlabId != self.slabId { + if handleSlabId & UNIFFI_SLAB_FOREIGN_BIT == 0 { + throw UniffiInternalError.slabError("Handle belongs to a Rust slab") + } else { + throw UniffiInternalError.slabError("slab ID mismatch") + } + } + let entry = entries[index] + switch entry { + case Entry.vacant: + throw UniffiInternalError.slabUseAfterFree("entry vacant") + + case Entry.occupied(let generation, _): + if generation != handleGeneration { + throw UniffiInternalError.slabUseAfterFree("generation mismatch") + } else { + return index + } + } + } + + private func makeHandle(generation: UInt64, index: Int) -> UInt64 { + return UInt64(index) | generation | self.slabId + } + + private mutating func getVacantToUse() throws -> Int { + if (next == UNIFFI_SLAB_END_OF_LIST) { + entries.append(Entry.vacant(next: UNIFFI_SLAB_END_OF_LIST)) + return entries.count - 1 + } else { + let oldNext = next + switch entries[next] { + case Entry.occupied: + throw UniffiInternalError.slabError("next is occupied") + case Entry.vacant(let next): + self.next = next + return oldNext + } + } + } + + {{ internal_component_vis }} mutating func insert(_ value: T) throws -> UInt64 { + return try lock.withLock { + let idx = try getVacantToUse() + let generation = self.generation + self.generation = (self.generation + UNIFFI_SLAB_GENERATION_UNIT) & UNIFFI_SLAB_GENERATION_MASK + entries[idx] = Entry.occupied(generation: generation, value: value) + return makeHandle(generation: generation, index: idx) + } + } + + {{ internal_component_vis }} mutating func remove(_ handle: UInt64) throws -> T { + return try lock.withLock { + let idx = try lookup(handle: handle) + switch entries[idx] { + case Entry.vacant: + throw UniffiInternalError.slabError("Entry vacant (and lookup missed it)") + + case Entry.occupied(_, let value): + entries[idx] = Entry.vacant(next: self.next) + self.next = idx + return value + } + } + } + + {{ internal_component_vis }} func get(_ handle: UInt64) throws -> T { + return try lock.withLock { + let idx = try lookup(handle: handle) + switch entries[idx] { + case Entry.vacant: + throw UniffiInternalError.slabError("Entry vacant (and lookup missed it)") + + case Entry.occupied(_, let value): + return value + } + } + } + + {{ internal_component_vis }} func specialValue(_ value: Int) -> UInt64 { + return UNIFFI_SLAB_MAX_ENTRIES + UInt64(value) + } +} diff --git a/uniffi_bindgen/src/bindings/swift/templates/wrapper.swift b/uniffi_bindgen/src/bindings/swift/templates/wrapper.swift index c34d348efb..f6acdf5118 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/wrapper.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/wrapper.swift @@ -15,6 +15,7 @@ import {{ config.ffi_module_name() }} {% include "RustBufferTemplate.swift" %} {% include "Helpers.swift" %} +{% include "Slab.swift" %} // Public interface members begin here. {{ type_helper_code }} diff --git a/uniffi_core/Cargo.toml b/uniffi_core/Cargo.toml index 7b9ab4fb72..547b66a7a4 100644 --- a/uniffi_core/Cargo.toml +++ b/uniffi_core/Cargo.toml @@ -21,9 +21,13 @@ once_cell = "1.10.0" # Enable "async" so that receivers implement Future, no need for "std" since we don't block on them. oneshot = { version = "0.1", features = ["async"] } # Regular dependencies +const-random = "0.1.15" paste = "1.0" static_assertions = "1.1.0" +[dev-dependencies] +rand = "0.8" + [features] default = [] # `no_mangle` RustBuffer FFI functions diff --git a/uniffi_core/src/ffi/ffidefault.rs b/uniffi_core/src/ffi/ffidefault.rs index 1f86f6b13b..527de0bb38 100644 --- a/uniffi_core/src/ffi/ffidefault.rs +++ b/uniffi_core/src/ffi/ffidefault.rs @@ -62,3 +62,9 @@ impl FfiDefault for Option { None } } + +impl FfiDefault for crate::Handle { + fn ffi_default() -> Self { + Self::from_raw(0) + } +} diff --git a/uniffi_core/src/ffi/mod.rs b/uniffi_core/src/ffi/mod.rs index b606323297..12cf4777b6 100644 --- a/uniffi_core/src/ffi/mod.rs +++ b/uniffi_core/src/ffi/mod.rs @@ -12,6 +12,7 @@ pub mod foreignexecutor; pub mod rustbuffer; pub mod rustcalls; pub mod rustfuture; +pub mod slab; pub use callbackinterface::*; pub use ffidefault::FfiDefault; @@ -21,3 +22,4 @@ pub use foreignexecutor::*; pub use rustbuffer::*; pub use rustcalls::*; pub use rustfuture::*; +pub use slab::*; diff --git a/uniffi_core/src/ffi/slab.rs b/uniffi_core/src/ffi/slab.rs new file mode 100644 index 0000000000..b1aecc86ba --- /dev/null +++ b/uniffi_core/src/ffi/slab.rs @@ -0,0 +1,462 @@ +/* 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/. */ + +//! Store Arc references owned by the foreign side and use handles to manage them +//! +//! This module defines the [Slab] class allows us to insert `Arc<>` values and use [Handle] values to manage the allocation. +//! It's named "Slab" because it's designed like a slab-allocator, for example the `tokio` `slab` crate (https://github.com/tokio-rs/slab). +//! +//! Usage: +//! * Create a `Slab` that will store Arc values. +//! * Call `insert()` to store a value and allocated a handle that represents a single strong ref. +//! * Pass the handle across the FFI to the foreign side. +//! * When the foreign side wants to use that value, it passes back the handle. +//! Use `get()` to get a reference to the stored value +//! * When the foreign side is finished with the value, it passes the handle to a free scaffolding function. +//! That function calls `remove` to free the allocation. +//! +//! Using handles to manage arc references provides several benefits: +//! * Handles are simple `u64` values, which are simpler to work with than pointers. +//! * The implementation is 100% safe code. +//! * Handles store a generation counter, which can usually detect use-after-free bugs. +//! * Handles store an slab id, which can usually detect using handles with the wrong Slab. +//! * Handles only use 48 bits, which makes them easier to work with on languages like JS that don't support full 64-bit integers. +//! Also languages like Kotlin, which prefer signed values, can treat them as signed without issues. +//! * Handles have a bit to differentiate between foreign-allocated handles and rust-allocated ones. +//! The trait interface code uses this to differentiate between Rust-implemented and foreign-implemented traits. + +use std::{fmt, sync::RwLock}; + +use const_random::const_random; + +// This code assumes that usize is at least as wide as u32 +static_assertions::const_assert!(std::mem::size_of::() >= std::mem::size_of::()); + +/// Slab error type +/// +/// This is extremely simple, since we almost always use the `*_or_panic` methods in scaffolding +/// code. +#[derive(Debug, PartialEq, Eq)] +pub enum Error { + SlabIdMismatch, + ForeignHandle, + UseAfterFree(&'static str), + OverCapacity, + Other(&'static str), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::UseAfterFree(msg) => write!(f, "Use-after-free detected ({msg})"), + Self::SlabIdMismatch => write!(f, "Slab id mismatch"), + Self::ForeignHandle => write!(f, "Handle belongs to a foreign slab"), + Self::OverCapacity => write!(f, "Slab capacity exceeded"), + Self::Other(msg) => write!(f, "Slab internal error: {msg}"), + } + } +} + +impl std::error::Error for Error {} + +pub type Result = std::result::Result; + +/// Handle for a value stored in the slab +/// +/// * The first 32 bits identify the value. +/// - The first 4,000,000,000 values are used for indexes in the `entries` table. +/// - The next values are reserved for special cases (see `rust_future.rs` for an example). +/// * The next 8 bits are for an slab id: +/// - The first bit is always 0, which indicates the handle came from Rust. +/// - The next 7 bits are initialized to a random value. +/// - this means that using a handle with the wrong Slab will be detected > 99% of the time. +/// * The next 8 bits are a generation counter value, this means that use-after-free bugs will be +/// detected until at least 256 inserts are performed after the free. +/// * The last 16 bits are intentionally unset, so that these can be easily used on languages like +/// JS that don't support full 64-bit integers. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[repr(transparent)] +pub struct Handle(u64); + +impl Handle { + pub const fn from_raw(val: u64) -> Self { + Self(val) + } + + pub const fn as_raw(&self) -> u64 { + self.0 + } + + pub fn is_from_rust(&self) -> bool { + (self.0 & FOREIGN_BIT) == 0 + } + + pub fn is_foreign(&self) -> bool { + (self.0 & FOREIGN_BIT) != 0 + } +} + +// 4 billion entries seems like plenty. Starting special values at 4,000,000,000, makes them +// easier to recognized when printed out. +const MAX_ENTRIES: u64 = 4_000_000_000; +const END_OF_LIST: usize = usize::MAX; +// Bit masks to isolate one part of the handle. +// Unit values are used to increment one segment of the handle. +// In general, we avoid bit shifts by updating the u64 value directly. +const INDEX_MASK: u64 = 0x0000_FFFF_FFFF; +const SLAB_ID_MASK: u64 = 0x00FF_0000_0000; +const SLAB_ID_UNIT: u64 = 0x0002_0000_0000; +const FOREIGN_BIT: u64 = 0x0001_0000_0000; +const GENERATION_MASK: u64 = 0xFF00_0000_0000; +const GENERATION_UNIT: u64 = 0x0100_0000_0000; + +/// Entry in the slab table. +/// +/// Note: We only use the `T: Clone` trait bound, but currently this is always used with Arc. +#[derive(Debug)] +enum Entry { + // Vacant entry. These form a kind of linked-list in the EntryList. Each inner value is the + // index of the next vacant entry. + Vacant { next: usize }, + Occupied { generation: u64, value: T }, +} + +/// Allocates handles that represent stored values and can be shared by the foreign code +pub struct Slab { + slab_id: u64, + inner: RwLock>, +} + +#[derive(Debug)] +struct SlabInner { + generation: u64, + next: usize, + entries: Vec>, +} + +impl Slab { + pub const fn new() -> Self { + Self::new_with_id(const_random!(u8)) + } + + pub const fn new_with_id(slab_id: u8) -> Self { + Self { + slab_id: (slab_id as u64 * SLAB_ID_UNIT) & SLAB_ID_MASK, + inner: RwLock::new(SlabInner { + generation: 0, + next: END_OF_LIST, + entries: vec![], + }), + } + } + + fn with_read_lock(&self, operation: F) -> R + where + F: FnOnce(&SlabInner) -> R, + { + let guard = self.inner.read().unwrap(); + operation(&*guard) + } + + fn with_write_lock(&self, operation: F) -> R + where + F: FnOnce(&mut SlabInner) -> R, + { + let mut guard = self.inner.write().unwrap(); + operation(&mut *guard) + } + + // Lookup a handle + // + // This inputs a handle, validates it's tag, generation counter, etc. + // Returns an index that points to an allocated entry. + fn lookup(&self, inner: &SlabInner, handle: Handle) -> Result { + if handle.is_foreign() { + return Err(Error::ForeignHandle); + } + let raw = handle.as_raw(); + let index = (raw & INDEX_MASK) as usize; + let handle_slab_id = raw & SLAB_ID_MASK; + let handle_generation = raw & GENERATION_MASK; + + if handle_slab_id != self.slab_id { + return Err(Error::SlabIdMismatch); + } + let entry = &inner.entries[index]; + match entry { + Entry::Vacant { .. } => Err(Error::UseAfterFree("vacant")), + Entry::Occupied { generation, .. } => { + if *generation != handle_generation { + Err(Error::UseAfterFree("generation mismatch")) + } else { + Ok(index) + } + } + } + } + + // Create a handle from an index + fn make_handle(&self, generation: u64, index: usize) -> Handle { + Handle(index as u64 | generation | self.slab_id) + } + + /// Find a vacant entry to insert a new item in + /// + /// This removes it from the vacancy list and returns the index + fn get_vacant_to_use<'a>(&self, inner: &'a mut SlabInner) -> Result { + match inner.next { + END_OF_LIST => { + // No vacant entries, push a new entry and use that + inner.entries.push(Entry::Vacant { next: END_OF_LIST }); + Ok(inner.entries.len() - 1) + } + old_next => { + // There's at least one vacant entry, pop it off the list + match inner.entries[old_next as usize] { + Entry::Occupied { .. } => return Err(Error::Other("self.next is occupied")), + Entry::Vacant { next } => { + inner.next = next; + } + }; + Ok(old_next as usize) + } + } + } + + /// Insert a new item into the Slab and get a Handle to access it with + pub fn insert(&self, value: T) -> Result { + self.with_write_lock(move |inner| { + let idx = self.get_vacant_to_use(inner)?; + let generation = inner.generation; + inner.generation = (inner.generation + GENERATION_UNIT) & GENERATION_MASK; + inner.entries[idx] = Entry::Occupied { value, generation }; + Ok(self.make_handle(generation, idx)) + }) + } + + /// Get a cloned value from a handle + pub fn get_clone(&self, handle: Handle) -> Result { + self.with_read_lock(|inner| { + let idx = self.lookup(inner, handle)?; + match &inner.entries[idx] { + Entry::Occupied { value, .. } => Ok(value.clone()), + // lookup ensures the entry is occupied + Entry::Vacant { .. } => unreachable!(), + } + }) + } + + // Remove an item from the slab, returning the original value + pub fn remove(&self, handle: Handle) -> Result { + self.with_write_lock(move |inner| { + let idx = self.lookup(inner, handle)?; + // Push the entry to the head of the vacant list + let old_value = + std::mem::replace(&mut inner.entries[idx], Entry::Vacant { next: inner.next }); + inner.next = idx; + match old_value { + Entry::Occupied { value, .. } => Ok(value), + // lookup ensures the entry is occupied + Entry::Vacant { .. } => unreachable!(), + } + }) + } + + pub fn insert_or_panic(&self, value: T) -> Handle { + self.insert(value).unwrap_or_else(|e| panic!("{e}")) + } + + pub fn get_clone_or_panic(&self, handle: Handle) -> T { + self.get_clone(handle).unwrap_or_else(|e| panic!("{e}")) + } + + pub fn remove_or_panic(&self, handle: Handle) -> T { + self.remove(handle).unwrap_or_else(|e| panic!("{e}")) + } + + // Create a special-cased handle + // + // This is guaranteed to not equal any real handles allocated by the slab. + pub const fn special_value(&self, val: u32) -> Handle { + let raw = MAX_ENTRIES + val as u64; + if (raw & !INDEX_MASK) != 0 { + panic!("special value too large"); + } + Handle::from_raw(raw) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::Arc; + + fn check_slab_size(slab: &Slab, occupied_count: usize, vec_size: usize) { + let inner = slab.inner.read().unwrap(); + assert_eq!( + inner + .entries + .iter() + .filter(|e| matches!(e, Entry::Occupied { .. })) + .count(), + occupied_count + ); + assert_eq!(inner.entries.len(), vec_size); + } + + #[test] + fn test_simple_usage() { + let slab = Slab::new(); + check_slab_size(&slab, 0, 0); + let handle1 = slab.insert(Arc::new("Hello")).unwrap(); + check_slab_size(&slab, 1, 1); + let handle2 = slab.insert(Arc::new("Goodbye")).unwrap(); + check_slab_size(&slab, 2, 2); + assert_eq!(*slab.get_clone(handle1).unwrap(), "Hello"); + slab.remove(handle1).unwrap(); + check_slab_size(&slab, 1, 2); + assert_eq!(*slab.get_clone(handle2).unwrap(), "Goodbye"); + slab.remove(handle2).unwrap(); + check_slab_size(&slab, 0, 2); + } + + #[test] + fn test_special_values() { + let slab = Slab::::new(); + assert_eq!(slab.special_value(0), Handle(4_000_000_000)); + assert_eq!(slab.special_value(1), Handle(4_000_000_001)); + } + + #[test] + fn test_slab_id_check() { + let slab = Slab::>::new_with_id(1); + let slab2 = Slab::>::new_with_id(2); + let handle = slab.insert(Arc::new("Hello")).unwrap(); + assert_eq!(Err(Error::SlabIdMismatch), slab2.get_clone(handle)); + assert_eq!(Err(Error::SlabIdMismatch), slab2.remove(handle)); + } + + #[test] + fn test_foreign_handle() { + let slab = Slab::>::new_with_id(1); + let handle = slab.insert(Arc::new("Hello")).unwrap(); + let foreign_handle = Handle::from_raw(handle.as_raw() | FOREIGN_BIT); + assert_eq!(Err(Error::ForeignHandle), slab.get_clone(foreign_handle)); + } +} + +#[cfg(test)] +mod stress_tests { + use super::*; + use anyhow::{bail, Result}; + use rand::{rngs::StdRng, RngCore, SeedableRng}; + use std::sync::Arc; + + // Single operation performed by the stress test. This exists so we can log operations and + // print them out when the tests fail. + #[derive(Debug)] + enum Operation { + Insert { value: usize }, + Remove { index: usize }, + } + + struct SlabStressTester { + rng: StdRng, + slab: Slab>, + next_value: usize, + allocated: Vec<(Handle, usize)>, + freed: Vec, + operations: Vec, + } + + impl SlabStressTester { + fn new(seed: u64) -> Self { + Self { + rng: StdRng::seed_from_u64(seed), + slab: Slab::new(), + next_value: 0, + allocated: vec![], + freed: vec![], + operations: vec![], + } + } + + fn perform_operation(&mut self) -> Result<()> { + if self.allocated.is_empty() || self.rng.next_u32() % 2 == 0 { + self.perform_insert() + } else { + self.perform_remove() + } + } + + fn perform_insert(&mut self) -> Result<()> { + self.operations.push(Operation::Insert { + value: self.next_value, + }); + let handle = self.slab.insert(Arc::new(self.next_value))?; + self.allocated.push((handle, self.next_value)); + self.next_value += 1; + Ok(()) + } + + fn perform_remove(&mut self) -> Result<()> { + let index = (self.rng.next_u32() as usize) % self.allocated.len(); + self.operations.push(Operation::Remove { index }); + + let (handle, expected) = self.allocated.remove(index); + let removed_value = self.slab.remove(handle)?; + if *removed_value != expected { + bail!("Removed value doesn't match: {removed_value} (expected: {expected})") + } + self.freed.push(handle); + Ok(()) + } + + fn check(&self) -> Result<()> { + // Test getting all handles, allocated or freed + for (handle, expected) in self.allocated.iter() { + let value = self.slab.get_clone(*handle)?; + if *value != *expected { + bail!("Value from get_clone doesn't match: {value} (expected: {expected})") + } + } + for handle in self.freed.iter() { + let result = self.slab.get_clone(*handle); + if !matches!(result, Err(Error::UseAfterFree(_))) { + bail!( + "Get clone on freed handle didn't return Error::UseAfterFree ({result:?})" + ) + } + } + Ok(()) + } + } + + #[test] + fn stress_test() { + for _ in 0..100 { + let mut tester = SlabStressTester::new(0); + let mut one_loop = || { + // Note; the inner loop is 255 elements, because that's the limit of insertions before + // our use-after-free detection can fail. + for _ in 0..255 { + tester.perform_operation()?; + tester.check()?; + } + Ok(()) + }; + one_loop().unwrap_or_else(|e: anyhow::Error| { + panic!( + "{e}\nOperations:\n{}", + tester + .operations + .into_iter() + .map(|operation| format!("{operation:?}")) + .collect::>() + .join("\n") + ) + }); + } + } +}