Skip to content
Merged
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
9 changes: 9 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ members = [
"uniffi/cpp/fixtures/type-flattening",
"uniffi/dart",
"uniffi/dart/fixtures/arithmetic",
"uniffi/dart/fixtures/input-scratch",
"uniffi/dart/fixtures/benchmarks",
"uniffi/dart/fixtures/bytes_types",
"uniffi/dart/fixtures/callbacks",
Expand Down
8 changes: 8 additions & 0 deletions uniffi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ Offering a change upstream needs maintainer approval each time, so most have no

uniffi-rs:

- Python prefers the running event loop to the configured callback fallback
([mozilla/uniffi-rs@f903c36c](https://github.com/mozilla/uniffi-rs/commit/f903c36c09fd396560e4dc28763a97a5c552e1c3)); the public setter keeps its 0.32 name.
Unlike upstream, an async callback still goes to the configured loop while it runs, since
a sync call may block the caller's loop on the callback.
- Kotlin refuses a destroyed object passed as an argument (kixelated/uniffi-rs@88d835d).
- Ruby is removed.
- `toml` is pinned to major 1. Upstream's `>=0.9, <2` spans two majors, so a generator that
Expand Down Expand Up @@ -109,6 +113,10 @@ uniffi-dart, on top of upstream `main` at acterglobal/uniffi-dart@72d26c1:
- RustBuffer and async result leak fixes, `liftAndFree` type arguments, non-null optionals
lowered through `toRustBuffer`, an exported `Display` as an enum's `toString()`, and
disposed objects refused as arguments.
- Input scratch is released on partial allocation failure, and disposal reserves status
storage before closing the handle, so allocation failure leaves it owned and retryable
([Uniffi-Dart/uniffi-dart@71dc9f8b](https://github.com/Uniffi-Dart/uniffi-dart/commit/71dc9f8b8f54cdfa6ef5d08f24e70cd93ef11d7d),
[@a2751c4c](https://github.com/Uniffi-Dart/uniffi-dart/commit/a2751c4cb0df3d28b5efab0bac1dadf439d9b163)).
- Record errors keep their converter name, and futures cancel per operation (moq's
`nix/uniffi-dart-record-error.patch` and `nix/uniffi-dart-cancellation.patch`).
- Another crate's library is imported with a `uniffi_<namespace>` prefix that its types,
Expand Down
14 changes: 14 additions & 0 deletions uniffi/dart/fixtures/callbacks/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,20 @@ pub struct InMemoryEventPersister {
events: Mutex<Vec<String>>,
}

static PERSISTER_DROP_COUNT: std::sync::atomic::AtomicUsize =
std::sync::atomic::AtomicUsize::new(0);

impl Drop for InMemoryEventPersister {
fn drop(&mut self) {
PERSISTER_DROP_COUNT.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
}
}

#[uniffi::export]
pub fn persister_drop_count() -> u64 {
PERSISTER_DROP_COUNT.load(std::sync::atomic::Ordering::SeqCst) as u64
}

#[uniffi::export]
impl InMemoryEventPersister {
#[uniffi::constructor]
Expand Down
28 changes: 28 additions & 0 deletions uniffi/dart/fixtures/callbacks/test/disposal_allocation_cases.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import 'package:test/test.dart';

import '../callbacks.dart';
import 'tracking_allocator.dart' as tracker;

void main() {
tearDown(tracker.calloc.reset);

test(
'foreign trait disposal stays retryable when status allocation fails',
() {
final before = persisterDropCount();
final owner = InMemoryEventPersister();
final persister = owner.asPersister();
tracker.calloc.failAfter = 0;
expect(() => (persister as dynamic).dispose(), throwsStateError);
tracker.calloc.failAfter = null;
(persister as dynamic).dispose();
expect(() => persister.load(), throwsStateError);
(persister as dynamic).dispose();
// The owner still holds its own reference.
expect(persisterDropCount(), before);
owner.dispose();
expect(persisterDropCount(), before + 1);
expect(tracker.calloc.live, isEmpty);
},
);
}
64 changes: 64 additions & 0 deletions uniffi/dart/fixtures/callbacks/test/disposal_allocation_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import 'dart:io';

import 'package:test/test.dart';

void main() {
test('disposal survives injected status allocation failure', () async {
// Run a child suite in an isolated copy of this package with an
// instrumented allocator. The child's build hooks replace the native
// library, so it must not share this run's package directory.
final work = Directory.systemTemp.createTempSync('disposal_allocation');
try {
await _copyPackage(Directory.current, work);
final pubGet = await Process.run(Platform.resolvedExecutable, [
'pub',
'get',
'--offline',
], workingDirectory: work.path);
print(pubGet.stdout);
print(pubGet.stderr);
expect(pubGet.exitCode, 0);
for (final name in ['callbacks.dart', 'uniffi_runtime.dart']) {
final file = File('${work.path}/$name');
final source = file.readAsStringSync();
final import = RegExp(r'''import ["']package:ffi/ffi.dart["'];''');
expect(import.allMatches(source), hasLength(1));
file.writeAsStringSync(
source.replaceFirst(
import,
'import "package:ffi/ffi.dart" hide calloc;\n'
'import "test/tracking_allocator.dart";',
),
);
}
final result = await Process.run(Platform.resolvedExecutable, [
'test',
'test/disposal_allocation_cases.dart',
'--reporter',
'expanded',
], workingDirectory: work.path);
print(result.stdout);
print(result.stderr);
expect(result.exitCode, 0);
} finally {
work.deleteSync(recursive: true);
}
}, timeout: Timeout(Duration(minutes: 3)));
}

Future<void> _copyPackage(Directory from, Directory to) async {
to.createSync(recursive: true);
for (final entity in from.listSync()) {
final name = entity.uri.pathSegments
.where((segment) => segment.isNotEmpty)
.last;
if (name == '.dart_tool') {
continue;
}
if (entity is Directory) {
await _copyPackage(entity, Directory('${to.path}/$name'));
} else {
File(entity.path).copySync('${to.path}/$name');
}
}
}
34 changes: 34 additions & 0 deletions uniffi/dart/fixtures/callbacks/test/tracking_allocator.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import 'dart:ffi';

import 'package:ffi/ffi.dart' as ffi;

class TrackingAllocator implements Allocator {
final live = <int, int>{};
int? failAfter;

@override
Pointer<T> allocate<T extends NativeType>(int byteCount, {int? alignment}) {
if (failAfter == 0) throw StateError('injected allocation failure');
if (failAfter != null) failAfter = failAfter! - 1;
final pointer = ffi.calloc.allocate<T>(byteCount, alignment: alignment);
live[pointer.address] = byteCount;
return pointer;
}

@override
void free(Pointer<NativeType> pointer) {
if (live.remove(pointer.address) == null) {
throw StateError('free of untracked pointer');
}
ffi.calloc.free(pointer);
}

void reset() {
failAfter = null;
for (final address in live.keys.toList()) {
free(Pointer<Void>.fromAddress(address));
}
}
}

final calloc = TrackingAllocator();
22 changes: 22 additions & 0 deletions uniffi/dart/fixtures/input-scratch/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[package]
name = "input_scratch"
version = "0.1.0"
edition = "2021"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html


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

[dependencies]
uniffi = { workspace = true }

[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 uniffi/dart/fixtures/input-scratch/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 uniffi/dart/fixtures/input-scratch/src/api.udl
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
namespace input_scratch {};
16 changes: 16 additions & 0 deletions uniffi/dart/fixtures/input-scratch/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#[uniffi::export]
pub fn owned_len(value: Vec<u8>) -> u64 {
value.len() as u64
}

#[uniffi::export]
pub fn optional_len(value: Option<String>) -> u64 {
value.map_or(0, |s| s.len() as u64)
}

#[uniffi::export]
pub fn optional_small(value: Option<u8>) -> u8 {
value.unwrap_or(0)
}

uniffi::include_scaffolding!("api");
40 changes: 40 additions & 0 deletions uniffi/dart/fixtures/input-scratch/test/input_scratch_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import 'dart:io';

import 'package:test/test.dart';

void main() {
test('scratch allocations balance on success and injected failure', () async {
// Compile a child suite with an instrumented allocator. Keep the production
// API unchanged and restore the generated files even when a test fails.
final originals = <File, String>{};
try {
for (final name in ['input_scratch.dart', 'uniffi_runtime.dart']) {
final file = File(name);
final source = file.readAsStringSync();
originals[file] = source;
final import = RegExp(r'''import ["']package:ffi/ffi.dart["'];''');
expect(import.allMatches(source), hasLength(1));
file.writeAsStringSync(
source.replaceFirst(
import,
'import "package:ffi/ffi.dart" hide calloc;\n'
'import "test/tracking_allocator.dart";',
),
);
}
final result = await Process.run(Platform.resolvedExecutable, [
'test',
'test/scratch_cases.dart',
'--reporter',
'expanded',
]);
print(result.stdout);
print(result.stderr);
expect(result.exitCode, 0);
} finally {
for (final entry in originals.entries) {
entry.key.writeAsStringSync(entry.value);
}
}
}, timeout: Timeout(Duration(minutes: 3)));
}
66 changes: 66 additions & 0 deletions uniffi/dart/fixtures/input-scratch/test/scratch_cases.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import 'dart:typed_data';

import 'package:test/test.dart';

import '../input_scratch.dart';
import 'tracking_allocator.dart' as tracker;

void main() {
ensureInitialized();
tearDown(tracker.calloc.reset);

test(
'owned and optional inputs release scratch after native consumption',
() {
for (var i = 0; i < 100; i++) {
expect(ownedLen(value: Uint8List.fromList([1, 2, 3])), 3);
expect(ownedLen(value: Uint8List(0)), 0);
expect(optionalLen(value: 'x' * 64), 64);
expect(optionalLen(value: ''), 0);
expect(optionalLen(value: null), 0);
expect(tracker.calloc.live, isEmpty);
}
},
);

test('scratch cleanup preserves the copied RustBuffer', () {
final input = Uint8List.fromList([1, 2, 3]);
final buffer = toRustBuffer(input);
try {
expect(tracker.calloc.live, isEmpty);
input[0] = 99;
expect(buffer.asUint8List(), [1, 2, 3]);
} finally {
buffer.free();
}
expect(tracker.calloc.live, isEmpty);
});

for (final optional in [false, true]) {
for (var failAt = 0; failAt < 3; failAt++) {
test(
'partial allocation failure releases scratch: optional=$optional, allocation=$failAt',
() {
// Payload, ForeignBytes, and RustCallStatus are allocated in that order.
tracker.calloc.failAfter = failAt;
try {
expect(
() => optional
? FfiConverterOptionalString.lower('x' * 64)
: toRustBuffer(Uint8List(64)),
throwsStateError,
);
} finally {
tracker.calloc.failAfter = null;
}
expect(tracker.calloc.live, isEmpty);
},
);
}
}

test('optional serialization failure retains no native scratch', () {
expect(() => optionalSmall(value: 256), throwsArgumentError);
expect(tracker.calloc.live, isEmpty);
});
}
34 changes: 34 additions & 0 deletions uniffi/dart/fixtures/input-scratch/test/tracking_allocator.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import 'dart:ffi';

import 'package:ffi/ffi.dart' as ffi;

class TrackingAllocator implements Allocator {
final live = <int, int>{};
int? failAfter;

@override
Pointer<T> allocate<T extends NativeType>(int byteCount, {int? alignment}) {
if (failAfter == 0) throw StateError('injected allocation failure');
if (failAfter != null) failAfter = failAfter! - 1;
final pointer = ffi.calloc.allocate<T>(byteCount, alignment: alignment);
live[pointer.address] = byteCount;
return pointer;
}

@override
void free(Pointer<NativeType> pointer) {
if (live.remove(pointer.address) == null) {
throw StateError('free of untracked pointer');
}
ffi.calloc.free(pointer);
}

void reset() {
failAfter = null;
for (final address in live.keys.toList()) {
free(Pointer<Void>.fromAddress(address));
}
}
}

final calloc = TrackingAllocator();
4 changes: 4 additions & 0 deletions uniffi/dart/fixtures/input-scratch/tests/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#[test]
fn input_scratch() -> anyhow::Result<()> {
uniffi_dart::testing::run_test("input_scratch", "src/api.udl", None)
}
Loading
Loading