Skip to content

Conversation

@kumulynja
Copy link

@kumulynja kumulynja commented Nov 22, 2025

This PR updates uniffi to 0.30.0 and with some other fixes I could make binding generation work with it for bdk-ffi which uses HashMap/Map and pro-macros, so possibly 2 of the 5 critical issues are solved with them as well, but I should probably add specific tests for them still.

A second pretty drastic change done here was moving from path based opening of the native libs to using Native Assets, which recently became a stable feature in both Dart and Flutter and is the recommended way now to use native binaries.

@spacebear21
Copy link
Collaborator

Nice work! Glad uniffi-dart is working for you. For payjoin-ffi we are stuck on uniffi 0.29 for the time being (see payjoin/rust-payjoin#1167) so it would be nice to separate the code fixes from the uniffi upgrade, such that we can maintain two branches to support 0.30 and keep supporting 0.29 for some time.

…compatibility

Updates the generated Dart code to use simple library names instead of
absolute paths for Android and Linux platforms. This makes the
generated code compatible with Dart's Native Assets system.

Changes:
- Android: "${Directory.current.path}/lib{libname}.so" → "lib{libname}.so"
- Linux: "${Directory.current.path}/lib{libname}.so" → "lib{libname}.so"
Introduces a `library_loading_strategy` configuration option that allows
users to choose between traditional directory-based library loading and
Native Assets mode.

Configuration options:
- `directory_path` (default): Uses `${Directory.current.path}` for
  Android/Linux, maintaining backward compatibility
- `native_assets`: Uses `DynamicLibrary.process()` for all platforms,
  enabling Dart Native Assets integration

Usage in uniffi.toml:
```toml
[bindings.dart]
library_loading_strategy = "native_assets"
The generate_dart_bindings function was ignoring the config_file_override
parameter when library_mode was true, always using the udl_file path
instead. This prevented custom configurations in uniffi.toml from being
read when generating bindings in library mode.

Fixed by using config_file_override when provided, falling back to
udl_file only when no override is specified:
  let config_path = config_file_override.unwrap_or(udl_file)

This ensures the library_loading_strategy and other custom configurations
are properly applied in library mode.
This is a major refactor that modernizes the FFI bindings to use Dart 3.10+'s
Native Assets system instead of manual DynamicLibrary.open() calls.

Key changes:
- Replace DynamicLibrary.open() with @Native annotations using asset IDs
- Generate Native Assets build hooks (hook/build.dart) using hooks package
- Implement asset ID system: package:{dart_package_name}/uniffi:{cdylib_name}
- Update test infrastructure to support Native Assets in test environments
- Add code_assets and hooks as dependencies in generated pubspec.yaml
- Remove runtime library path resolution (handled by Native Assets)

Technical details:
- Asset IDs use format: "package:{package}/uniffi:{cdylib}"
- Build hooks register simple names; Dart auto-prefixes with package path
- Generated Dart code constructs full asset ID for @Native annotations
- Preserved library_file parameter (needed for proc macro metadata extraction)

Benefits:
- No manual library loading required
- Cross-platform library resolution handled automatically
- Better integration with Dart's build system
- Simpler generated code with declarative @Native functions

All 19 tests passing with new Native Assets implementation.
@kumulynja kumulynja changed the title Update to uniffi 0.30.0 + fixes Shift to Native Assets Nov 24, 2025
@kumulynja kumulynja changed the title Shift to Native Assets Shift to Native Assets + update to uniffi 0.30.0 Nov 24, 2025
@kumulynja
Copy link
Author

kumulynja commented Nov 24, 2025

@spacebear21 Since I was a bit stuck with how to bundle/package binaries to publish packages with generated bindings on pub.dev or easily use the packages from other projects, I made quite radical changes, but I think they are worth it:
This PR refactors uniffi-dart to use Native Assets, the recent and recommended way to use native libraries in Dart/Flutter.

The main advantage it has is that the native lib shouldn't be pre-compiled somewhere or cargo-kit shouldn't be used to hook into the Flutter build process anymore. With Native Assets and native_toolchain_rs a build hook does all of this for the correct target automatically.
This makes it also possible to use the bindings for pure Dart CLI's, server side dart and Flutter apps (Desktop/iOS/Android) all without any custom configurations per platform.

I think this is the way to go, but of course this needs more eyes and thorough testing. As of now it was tested only very limited with bdk-dart here: bitcoindevkit/bdk-dart#12
As you can see, no special config is needed for neither ios or the example apps. The build hook does the magic dynamically.

Let me know what you think

- Add free + clone entries to callback vtables (0.30.0 layout)
- Implement proper handle-cloning via UniffiHandleMap for callbacks
- Wire uniffiFree/uniffiClone into vtable init
- Add trait handle LSB check and reject foreign trait implementations
…nals

When a primitive type (like u16) was only used within optional fields (u16?)
and never as a bare type, uniffi-dart would generate FfiConverterOptionalUInt16
that referenced FfiConverterUInt16, but would never generate the base converter
itself, causing compilation errors.

The issue was in the compound type (Optional/Sequence) rendering logic, which
only rendered inner helpers for Sequence types. Now it also renders helpers
for primitive types (Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64,
Float32, Float64) when they haven't been registered yet.

This ensures that when Optional<UInt16> is encountered, both FfiConverterUInt16
and FfiConverterOptionalUInt16 are generated.
The previous implementation of rustCall would call the FFI function and
immediately deserialize the result before checking if an error occurred.
When a Rust function throws an error, the FFI returns garbage data in the
result buffer, causing RangeError or other deserialization failures when
Dart tries to lift the invalid data.

This fix introduces rustCallWithLifter which:
1. Calls the FFI function to get the raw result
2. Checks the error status
3. Only deserializes (lifts) the result if no error occurred

Changes:
- Add rustCallWithLifter() function in types.rs that separates FFI call
  from result lifting
- Update function generation in functions.rs to use rustCallWithLifter
- Update object method generation in objects.rs to use rustCallWithLifter
- Keep original rustCall() for void-returning functions

This ensures error handling happens before any deserialization, preventing
crashes when functions throw expected errors.
spacebear21 added a commit to spacebear21/rust-payjoin that referenced this pull request Nov 26, 2025
With the changes from
Uniffi-Dart/uniffi-dart#99, the native libraries
can be compiled automagically in a dart build hook.
spacebear21 added a commit to spacebear21/rust-payjoin that referenced this pull request Nov 26, 2025
With the changes from
Uniffi-Dart/uniffi-dart#99, the native libraries
can be compiled automagically in a dart build hook.
Per the uniffi changelog: "Foreign handles must always have the lowest
bit set".
spacebear21 added a commit to spacebear21/rust-payjoin that referenced this pull request Nov 26, 2025
With the changes from
Uniffi-Dart/uniffi-dart#99, the native libraries
can be compiled automagically in a dart build hook.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants