Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 51 additions & 1 deletion .github/workflows/test-downstream.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,24 @@ jobs:
uniffi-dart = { path = "../main" }
EOF

- name: Bump payjoin-ffi to uniffi 0.32
# rust-payjoin still pins uniffi 0.31.2; its 0.31 metadata cannot be read
# by this PR's 0.32 bindgen (`Unexpected metadata type code`). Bump its
# own uniffi to 0.32 so the metadata matches — payjoin-ffi compiles and
# its Dart suite passes unchanged on 0.32 (probed: 24 tests). Removed once
# rust-payjoin ships 0.32 upstream.
run: |
sed -i 's/uniffi = { version = "0.31.2"/uniffi = { version = "0.32"/g' rust-payjoin/payjoin-ffi/Cargo.toml
# payjoin's native-assets build pins rustc 1.85.1 (< uniffi 0.32's 1.91
# floor). native_toolchain_rust requires an *exact* channel and a
# `targets` list, so pin 1.91.0 with just the Linux host target (the CI
# only builds for the host).
for f in $(find rust-payjoin -name rust-toolchain.toml); do
printf '[toolchain]\nchannel = "1.91.0"\ntargets = ["x86_64-unknown-linux-gnu"]\n' > "$f"
done
echo "--- payjoin-ffi uniffi pins now ---"
grep -n 'uniffi = ' rust-payjoin/payjoin-ffi/Cargo.toml || true

- name: Generate dart bindings and run tests
run: cd rust-payjoin/payjoin-ffi/dart && bash scripts/generate_bindings.sh && dart test

Expand Down Expand Up @@ -71,13 +89,45 @@ jobs:
- name: Use cache
uses: Swatinem/rust-cache@v2

- name: Patch uniffi-dart dependency
- name: Fetch and bump bdk-ffi to uniffi 0.32
# bdk-dart re-exports bdk-ffi, pinned by git rev at uniffi 0.31.2 — so the
# metadata bdk_dart_ffi embeds is 0.31 and this PR's 0.32 bindgen cannot
# read it. Clone that exact rev, bump its uniffi to 0.32 (it compiles
# clean on 0.32), and `[patch]` bdk-dart onto the local copy. Removed once
# bdk-ffi ships 0.32 upstream.
run: |
BDK_FFI_REV=17c48b8b52ba81cdc58531e75ad1165be0cc25d9
git init -q bdk-ffi
git -C bdk-ffi remote add origin https://github.com/bitcoindevkit/bdk-ffi.git
git -C bdk-ffi fetch --depth 1 origin "$BDK_FFI_REV" -q
git -C bdk-ffi checkout -q FETCH_HEAD
sed -i 's/uniffi = { version = "=0.31.2"/uniffi = { version = "0.32"/g' bdk-ffi/bdk-ffi/Cargo.toml
echo "--- bdk-ffi uniffi pins now ---"
grep -n 'uniffi = ' bdk-ffi/bdk-ffi/Cargo.toml || true

- name: Patch bdk-dart onto uniffi 0.32 (uniffi, uniffi-dart, bdk-ffi)
run: |
# bdk-dart's own native crate also pins uniffi 0.31.2 — bump it too.
sed -i 's/uniffi = { version = "=0.31.2"/uniffi = { version = "0.32"/g' bdk-dart/native/Cargo.toml
cat >> bdk-dart/native/Cargo.toml << 'EOF'

[patch.'https://github.com/Uniffi-Dart/uniffi-dart']
uniffi-dart = { path = "../../main" }

[patch.'https://github.com/bitcoindevkit/bdk-ffi.git']
bdk-ffi = { path = "../../bdk-ffi/bdk-ffi" }
EOF
# Lockfile pins uniffi 0.31.2 / the old bdk-ffi rev; drop it so the bump
# and patches re-resolve cleanly.
rm -f bdk-dart/native/Cargo.lock

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current BDK script uses cargo build --locked and cargo run --locked, so deleting the lockfile here leaves it unable to run.

Please regenerate the lockfile after applying the patches and before calling the script. I reproduced the failure without that step; with it, the unchanged script proceeds.

# bdk-dart/bdk-ffi pin rustc 1.85.1, but uniffi 0.32 needs 1.91.
# native_toolchain_rust requires an *exact* channel and a `targets`
# list, so pin 1.91.0 with just the Linux host target.
for f in $(find bdk-dart bdk-ffi -name rust-toolchain.toml); do
printf '[toolchain]\nchannel = "1.91.0"\ntargets = ["x86_64-unknown-linux-gnu"]\n' > "$f"
done
echo "--- bdk-dart native uniffi pins now ---"
grep -n 'uniffi = ' bdk-dart/native/Cargo.toml || true

- name: Generate dart bindings and run tests
run: cd bdk-dart && bash scripts/generate_bindings.sh && dart test
4 changes: 2 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
matrix:
rust:
- stable
- "1.85"
- "1.91"
- nightly
steps:
- name: Checkout sources
Expand Down Expand Up @@ -47,7 +47,7 @@ jobs:
matrix:
rust:
- stable
- "1.85"
- "1.91"
- nightly
steps:
- name: Checkout sources
Expand Down
19 changes: 13 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ edition = "2021"
license = "Apache-2 or MIT"
homepage = "https://github.com/acterglobal/uniffi-dart"
description = "Dart Frontend for UniFFI"
rust-version = "1.85"
rust-version = "1.91"

[features]
defaults = []
Expand Down Expand Up @@ -38,7 +38,13 @@ uniffi_bindgen = { workspace = true }
camino = "1"
cargo_metadata = "0.18"
serde = "1"
toml = ">=0.8, <=0.9"
# Match uniffi_bindgen 0.32's own toml: its `BindingGenerator::new_config` takes
# `&toml::Value`, so our impl must resolve the SAME toml version or the trait
# signature mismatches (E0053). uniffi_bindgen 0.32.0 declares `toml = ">=0.9,
# <2"` and resolves to 1.x, so pin `toml = "1"`; a looser pin lets the resolver
# put us on 0.9 while uniffi stays on 1.x (splitting the crate) — which has no
# committed Cargo.lock to stabilize it, so CI re-resolves and breaks.
toml = "1"
genco = "0.17.5"
proc-macro2 = "1.0.66"

Expand Down Expand Up @@ -82,12 +88,13 @@ members = [
"fixtures/proc-macro",
"fixtures/proc-macro-no-implicit-prelude",
"fixtures/enum_variant_collision",
"fixtures/map_type",
#"fixtures/*",
]

[workspace.dependencies]
uniffi = { version = "0.31.2" }
uniffi_bindgen = { version = "0.31.2" }
uniffi_build = { version = "0.31.2" }
uniffi_testing = { version = "0.31.2" }
uniffi = { version = "0.32" }

@chavic chavic Sep 11, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following up on BeyondTranslate: it tracks main in all three generator dependency sections, but pins UniFFI to =0.31.2 and has no committed lockfile. A fresh resolution after this merge gives it a 0.32 generator with 0.31.2 metadata. I reproduced that combination failing with Unexpected metadata type code 97.

Just flagging the downstream impact here. We don't need to hold this PR for a change in their repo. After we merge, a follow-up PR pinning their generator to a compatible revision would be kind; then they can move to 0.32 separately.

uniffi_bindgen = { version = "0.32" }
uniffi_build = { version = "0.32" }
uniffi_testing = { version = "0.32" }
camino = { version = "1.1" }
2 changes: 1 addition & 1 deletion fixtures/benchmarks/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ crate-type = ["lib", "cdylib"]
bench = false

[dependencies]
uniffi = "0.31"
uniffi = { workspace = true }
clap = { version = "4", features = ["cargo", "std", "derive"] }
criterion = "0.5.1"

Expand Down
7 changes: 7 additions & 0 deletions fixtures/bytes_types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ fn take_bytes(v: Vec<u8>) -> Vec<u8> {
v
}

// Borrowed `&[u8]` argument: uniffi 0.32 lowers this through the zero-copy
// `ForeignBytes` FFI path rather than the owned `RustBuffer` path.
#[uniffi::export]
fn take_bytes_by_ref(v: &[u8]) -> Vec<u8> {
v.to_vec()
}

#[uniffi::export]
fn take_bytes_with_validation(v: Vec<u8>) -> Vec<u8> {
// Validate that it's valid UTF-8 if it should be
Expand Down
11 changes: 11 additions & 0 deletions fixtures/bytes_types/test/bytes_types_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ void main() {
expect(result, equals(input));
});

test('take_bytes_by_ref (&[u8], ForeignBytes path) returns same data', () {
final input = [10, 20, 30, 40, 50];
final result = takeBytesByRef(v: Uint8List.fromList(input));
expect(result, equals(input));
});

test('take_bytes_by_ref handles empty bytes', () {
final result = takeBytesByRef(v: Uint8List.fromList([]));
expect(result, isEmpty);
});

// test('take_bytes_with_validation handles UTF-8', () {
// final utf8Input = 'Hello, 世界!'.codeUnits;
// final result = takeBytesWithValidation(utf8Input);
Expand Down
2 changes: 1 addition & 1 deletion fixtures/docstring-proc-macro/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ crate-type = ["lib", "cdylib"]

[dependencies]
thiserror = "1.0"
uniffi = "0.31"
uniffi = { workspace = true }

[build-dependencies]
uniffi-dart = { path = "../../", features = ["build"] }
Expand Down
24 changes: 24 additions & 0 deletions fixtures/map_type/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[package]
name = "map_type"
version = "0.1.0"
edition = "2021"
publish = false

[lib]
name = "map_type"
crate-type = ["lib", "cdylib"]

[dependencies]
uniffi = { workspace = true, features = [
"build",
] }

[build-dependencies]
uniffi-dart = { path = "../../", features = ["build"] }

[dev-dependencies]
uniffi-dart = { path = "../../", features = ["bindgen-tests"] }
uniffi = { workspace = true, features = [
"bindgen-tests",
] }
anyhow = "1"
3 changes: 3 additions & 0 deletions fixtures/map_type/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
fn main() {
uniffi_dart::generate_scaffolding("./src/api.udl".into()).unwrap();
}
1 change: 1 addition & 0 deletions fixtures/map_type/src/api.udl
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
namespace map_type { };
43 changes: 43 additions & 0 deletions fixtures/map_type/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
use std::collections::HashMap;

// Minimal Map<K, V> round-trip surface. Mirrors how our qdrant-edge-ffi crate
// exposes payload/config maps: plain proc-macro exports over HashMap<String, _>.

#[uniffi::export]
pub fn roundtrip_map(m: HashMap<String, i32>) -> HashMap<String, i32> {
m
}

#[uniffi::export]
pub fn count_entries(m: HashMap<String, i32>) -> u32 {
m.len() as u32
}

#[uniffi::export]
pub fn map_with_record_values(m: HashMap<String, Point>) -> HashMap<String, Point> {
m
}

// Variable-length value converters (nested Map, Option) are what stress the Map
// FfiConverter's offset arithmetic — a fixed-size value (i32/Point) can't reveal
// a drifting offset. These mirror real payload shapes: string -> nested/nullable.

#[uniffi::export]
pub fn roundtrip_nested_map(
m: HashMap<String, HashMap<String, i32>>,
) -> HashMap<String, HashMap<String, i32>> {
m
}

#[uniffi::export]
pub fn roundtrip_optional_map(m: HashMap<String, Option<i32>>) -> HashMap<String, Option<i32>> {
m
}

#[derive(uniffi::Record, Clone)]
pub struct Point {
x: i64,
y: i64,
}

uniffi::include_scaffolding!("api");
47 changes: 47 additions & 0 deletions fixtures/map_type/test/map_type_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import 'package:test/test.dart';
import '../map_type.dart';

void main() {
test('map roundtrip preserves entries', () {
final m = {'a': 1, 'b': 2};
final out = roundtripMap(m: m);
expect(out['a'], 1);
expect(out['b'], 2);
expect(out.length, 2);
});

test('map count', () {
expect(countEntries(m: {'x': 10, 'y': 20, 'z': 30}), 3);
});

test('map with record values', () {
final out = mapWithRecordValues(m: {'origin': Point(x: 0, y: 0), 'unit': Point(x: 1, y: 1)});
expect(out['unit']!.x, 1);
expect(out['unit']!.y, 1);
expect(out.length, 2);
});

test('nested map round-trips (variable-length values)', () {
final out = roundtripNestedMap(m: {
'a': {'x': 1, 'y': 2},
'b': {'z': 3},
});
expect(out['a']!['x'], 1);
expect(out['a']!['y'], 2);
expect(out['b']!['z'], 3);
expect(out.length, 2);
});

test('map with optional values round-trips (null preserved, distinct from absent)', () {
final out = roundtripOptionalMap(m: {'present': 7, 'absent': null});
expect(out['present'], 7);
expect(out['absent'], null);
expect(out.containsKey('absent'), true);
expect(out.length, 2);
});

test('empty map round-trips at the length-0 boundary', () {
expect(roundtripMap(m: {}), isEmpty);
expect(countEntries(m: {}), 0);
});
}
6 changes: 6 additions & 0 deletions fixtures/map_type/tests/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
use anyhow::Result;

#[test]
fn map_type() -> Result<()> {
uniffi_dart::testing::run_test("map_type", "src/api.udl", None)
}
2 changes: 1 addition & 1 deletion fixtures/metadata/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ crate-type = ["lib", "cdylib"]

[dependencies]
thiserror = "1.0"
uniffi = "0.31"
uniffi = { workspace = true }
uniffi-dart = { path = "../../", features = ["bindgen-tests"] }

[build-dependencies]
Expand Down
45 changes: 45 additions & 0 deletions fixtures/proc-macro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,25 @@ impl Object {
Arc::new(Self)
}

// Constructor taking a borrowed `&[u8]` — exercises the sync constructor
// initializer-list call site (`_ptr = using((Arena _uniffiArena) => rustCall(...))`,
// via `wrap_ffi_call_expr`), a distinct generator from the method/function ones.
#[uniffi::constructor]
fn from_bytes(data: &[u8]) -> Arc<Self> {
let _ = data.len();
Arc::new(Self)
}

fn is_heavy(&self) -> MaybeBool {
MaybeBool::Uncertain
}

// Borrowed `&[u8]` on a *method* — exercises the object method call site's
// borrowed-bytes free (distinct from the free-function one).
fn borrowed_bytes_len(&self, data: &[u8]) -> u64 {
data.len() as u64
}

fn get_trait(&self, inc: Option<Arc<dyn Trait>>) -> Arc<dyn Trait> {
inc.unwrap_or_else(|| Arc::new(TraitImpl {}))
}
Expand Down Expand Up @@ -199,4 +214,34 @@ pub fn callback_get_other_multiply(
callback.get_other_callback_interface().multiply(a, b)
}

// Borrowed `&[u8]` argument (proc-macro `by_ref`): exercises the `ForeignBytes`
// lowering and the arena-scoped free the call site emits around it. Sums the
// bytes so a caller can assert the buffer content crossed the FFI boundary
// intact — and, called in a loop, that the per-call native copy does not leak.
#[uniffi::export]
pub fn sum_borrowed_bytes(data: &[u8]) -> u64 {
data.iter().map(|&b| u64::from(b)).sum()
}

// A fallible variant, so the throwing call-site shape (which must still free the
// borrowed copy on the error path) is exercised. Errors on a NON-empty input (a
// leading 0xFF sentinel) so the error path unwinds with a real data buffer to
// free through `using`, not merely the empty `(null, 0)` struct.
#[uniffi::export]
pub fn sum_borrowed_bytes_checked(data: &[u8]) -> Result<u64, BasicError> {
if data.first() == Some(&0xFF) {
return Err(BasicError::OsError);
}
Ok(data.iter().map(|&b| u64::from(b)).sum())
}

// Void return with a borrowed `&[u8]`: exercises the `void` call-site branch
// (`rustCall((status){...})`) wrapped in the arena — distinct from the non-void
// `rustCallWithLifter` branch the functions above hit.
#[uniffi::export]
pub fn consume_borrowed_bytes(data: &[u8]) {
// Touch the bytes so the argument is genuinely lowered/read.
let _ = data.iter().fold(0u64, |acc, &b| acc.wrapping_add(u64::from(b)));
}

uniffi::include_scaffolding!("api");
Loading
Loading