From 4ac273cca3e45eabb9fef504e6ef9540d15a900c Mon Sep 17 00:00:00 2001 From: vm75 Date: Wed, 2 Sep 2026 20:54:05 -0700 Subject: [PATCH 1/6] chore: upgrade wasm_ffi dependency to ^2.4.0 --- example/pubspec.yaml | 4 ++++ example_ffi_plugin/example/pubspec.yaml | 4 ++++ example_ffi_plugin/pubspec.yaml | 4 ++++ pubspec.yaml | 8 ++++++-- 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 030dd1f..ed3b5f8 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -16,3 +16,7 @@ dev_dependencies: build_web_compilers: ^4.0.9 lints: ^4.0.0 ffigen: ^14.0.0 + +dependency_overrides: + wasm_ffi: + path: ../../wasm_ffi diff --git a/example_ffi_plugin/example/pubspec.yaml b/example_ffi_plugin/example/pubspec.yaml index 643dae7..14b9b44 100644 --- a/example_ffi_plugin/example/pubspec.yaml +++ b/example_ffi_plugin/example/pubspec.yaml @@ -19,5 +19,9 @@ dev_dependencies: sdk: flutter flutter_lints: ^5.0.0 +dependency_overrides: + wasm_ffi: + path: ../../../wasm_ffi + flutter: uses-material-design: true diff --git a/example_ffi_plugin/pubspec.yaml b/example_ffi_plugin/pubspec.yaml index 5fc0a86..f3300da 100644 --- a/example_ffi_plugin/pubspec.yaml +++ b/example_ffi_plugin/pubspec.yaml @@ -21,6 +21,10 @@ dev_dependencies: sdk: flutter flutter_lints: ^5.0.0 +dependency_overrides: + wasm_ffi: + path: ../../wasm_ffi + flutter: plugin: platforms: diff --git a/pubspec.yaml b/pubspec.yaml index faab685..c7defc4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: universal_ffi description: A drop-in replacement for dart:ffi for all platforms including web (using wasm_ffi). -version: 1.5.0 +version: 1.5.1 repository: https://github.com/vm75/universal_ffi environment: @@ -9,12 +9,16 @@ environment: dependencies: ffi: ^2.1.5 path: ^1.9.1 - wasm_ffi: ^2.3.0 + wasm_ffi: ^2.4.0 dev_dependencies: lints: ^6.1.0 test: ^1.29.0 +dependency_overrides: + wasm_ffi: + path: ../wasm_ffi + topics: - ffi - wasm From baf75faf30ed5ed0baefd6983dcf9f7b29828bfb Mon Sep 17 00:00:00 2001 From: vm75 Date: Wed, 2 Sep 2026 20:54:07 -0700 Subject: [PATCH 2/6] test: add unit, integration, and browser smoke test suites --- example_ffi_plugin/example/lib/main.dart | 5 +- test/helper_test.dart | 75 +++++++++++ test/native_integration_test.dart | 85 ++++++++++++ test/path_resolution_test.dart | 101 +++++++++++++++ test/standalone_test_module.c | 18 +++ test/standalone_test_module.wasm | Bin 0 -> 929 bytes test/web_integration_test.dart | 76 +++++++++++ tool/browser_smoke.mjs | 158 +++++++++++++++++++++++ 8 files changed, 517 insertions(+), 1 deletion(-) create mode 100644 test/helper_test.dart create mode 100644 test/native_integration_test.dart create mode 100644 test/path_resolution_test.dart create mode 100644 test/standalone_test_module.c create mode 100755 test/standalone_test_module.wasm create mode 100644 test/web_integration_test.dart create mode 100644 tool/browser_smoke.mjs diff --git a/example_ffi_plugin/example/lib/main.dart b/example_ffi_plugin/example/lib/main.dart index c8209bb..2ead97e 100644 --- a/example_ffi_plugin/example/lib/main.dart +++ b/example_ffi_plugin/example/lib/main.dart @@ -50,7 +50,7 @@ class _AsyncRunnerWidgetState extends State { // Simulated asynchronous runner. Future> fetchValues() async { await init(widget.libPath); - return { + final results = { 'Library Name': getLibraryName(), 'Hello String': hello(widget.libPath), 'Size of Int': sizeOfInt().toString(), @@ -58,6 +58,9 @@ class _AsyncRunnerWidgetState extends State { 'Size of Pointer': sizeOfPointer().toString(), 'Static Init Check': staticInitCheck().toString(), }; + // ignore: avoid_print + print('[RESULT] ${widget.libPath}: $results'); + return results; } // Load data using the asynchronous runner diff --git a/test/helper_test.dart b/test/helper_test.dart new file mode 100644 index 0000000..51e496c --- /dev/null +++ b/test/helper_test.dart @@ -0,0 +1,75 @@ +import 'package:test/test.dart'; +import 'package:universal_ffi/ffi.dart'; +import 'package:universal_ffi/ffi_helper.dart'; +import 'package:universal_ffi/ffi_utils.dart'; + +class _TrackingAllocator implements Allocator { + int allocations = 0; + int deallocations = 0; + + @override + Pointer allocate(int byteCount, {int? alignment}) { + allocations++; + return calloc.allocate(byteCount, alignment: alignment); + } + + @override + void free(Pointer pointer) { + deallocations++; + calloc.free(pointer); + } +} + +void main() { + group('FfiHelper allocator helpers', () { + test( + 'safeUsing releases allocations using default or provided allocator', + () async { + final helper = await FfiHelper.load( + '', + options: {LoadOption.isStaticallyLinked}, + ); + final tracker = _TrackingAllocator(); + + final result = helper.safeUsing((Arena arena) { + final ptr = arena.allocate(10); + expect(ptr.address, isNot(0)); + return 42; + }, tracker); + + expect(result, equals(42)); + expect(tracker.allocations, equals(1)); + expect(tracker.deallocations, equals(1)); + }, + ); + + test('safeWithZoneArena runs computation in zoned arena', () async { + final helper = await FfiHelper.load( + '', + options: {LoadOption.isStaticallyLinked}, + ); + final tracker = _TrackingAllocator(); + + final result = helper.safeWithZoneArena(() { + final ptr = tracker.allocate(16); + tracker.free(ptr); + return 'success'; + }, tracker); + + expect(result, equals('success')); + expect(tracker.allocations, equals(1)); + expect(tracker.deallocations, equals(1)); + }); + + test('load throws ArgumentError for statically linked on Web', () { + // On native it succeeds for DynamicLibrary.process(), + // Web throws ArgumentError. + if (appType == AppType.web) { + expect( + () => FfiHelper.load('', options: {LoadOption.isStaticallyLinked}), + throwsArgumentError, + ); + } + }); + }); +} diff --git a/test/native_integration_test.dart b/test/native_integration_test.dart new file mode 100644 index 0000000..0ec1f7b --- /dev/null +++ b/test/native_integration_test.dart @@ -0,0 +1,85 @@ +@TestOn('vm') +library; + +import 'dart:io' show Directory, Platform; +import 'package:path/path.dart' as path; +import 'package:test/test.dart'; +import 'package:universal_ffi/ffi.dart'; +import 'package:universal_ffi/ffi_helper.dart'; +import 'package:universal_ffi/ffi_utils.dart'; + +// ignore_for_file: camel_case_types +typedef GetLibraryNameNative = Pointer Function(); +typedef GetLibraryNameDart = Pointer Function(); + +typedef HelloNative = Pointer Function(Pointer); +typedef HelloDart = Pointer Function(Pointer); + +typedef IntSizeNative = Int Function(); +typedef IntSizeDart = int Function(); + +typedef BoolSizeNative = Int Function(); +typedef BoolSizeDart = int Function(); + +typedef PointerSizeNative = Int Function(); +typedef PointerSizeDart = int Function(); + +typedef StaticInitCheckNative = Int Function(); +typedef StaticInitCheckDart = int Function(); + +void main() { + group('Native integration', () { + late String modulePath; + + setUpAll(() { + final assetsDir = path.join(Directory.current.path, 'example', 'assets'); + if (Platform.isLinux || Platform.isAndroid) { + modulePath = path.join(assetsDir, 'native_example'); + } else if (Platform.isMacOS || Platform.isIOS) { + modulePath = path.join(assetsDir, 'native_example'); + } else if (Platform.isWindows) { + modulePath = path.join(assetsDir, 'native_example'); + } + }); + + test('loads native library and executes functions via FfiHelper', () async { + final helper = await FfiHelper.load(modulePath); + expect(helper.library, isNotNull); + + final getLibraryName = helper.library + .lookupFunction( + 'getLibraryName', + ); + final libName = getLibraryName().cast().toDartString(); + expect(libName, equals('native_example')); + + final hello = helper.library.lookupFunction( + 'hello', + ); + final greeting = helper.safeUsing((Arena arena) { + final cStr = 'UniversalFFI'.toNativeUtf8(allocator: arena).cast(); + return hello(cStr).cast().toDartString(); + }); + expect(greeting, equals('Hello UniversalFFI!')); + + final intSize = helper.library.lookupFunction( + 'intSize', + ); + expect(intSize(), equals(sizeOf())); + + final boolSize = helper.library + .lookupFunction('boolSize'); + expect(boolSize(), isPositive); + + final pointerSize = helper.library + .lookupFunction('pointerSize'); + expect(pointerSize(), equals(sizeOf>())); + + final staticInitCheck = helper.library + .lookupFunction( + 'static_init_check', + ); + expect(staticInitCheck(), equals(1)); + }); + }); +} diff --git a/test/path_resolution_test.dart b/test/path_resolution_test.dart new file mode 100644 index 0000000..118f345 --- /dev/null +++ b/test/path_resolution_test.dart @@ -0,0 +1,101 @@ +import 'dart:io' show Platform; +import 'package:test/test.dart'; +import 'package:universal_ffi/ffi_helper.dart'; +import 'package:universal_ffi/src/dart_ffi/_ffi_helper.dart' as dart_ffi; +import 'package:universal_ffi/src/wasm_ffi/_ffi_helper.dart' as wasm_ffi; + +void main() { + group('Web path resolution', () { + test('defaults to .js when no extension and standalone is false', () { + final resolved = wasm_ffi.resolveModulePath('my_module', {}); + expect(resolved, equals('my_module.js')); + }); + + test('resolves to .wasm when isStandaloneWasm is set', () { + final resolved = wasm_ffi.resolveModulePath('my_module', { + LoadOption.isStandaloneWasm, + }); + expect(resolved, equals('my_module.wasm')); + }); + + test('preserves existing extension .wasm', () { + final resolved = wasm_ffi.resolveModulePath('custom/path.wasm', {}); + expect(resolved, equals('custom/path.wasm')); + }); + + test('preserves existing extension .js', () { + final resolved = wasm_ffi.resolveModulePath('custom/path.js', {}); + expect(resolved, equals('custom/path.js')); + }); + + test('formats plugin asset path when isFfiPlugin is set', () { + final resolved = wasm_ffi.resolveModulePath('native_example.wasm', { + LoadOption.isFfiPlugin, + }); + expect( + resolved, + equals('assets/packages/native_example/assets/native_example.wasm'), + ); + }); + + test('formats plugin asset path with inferred extension', () { + final resolved = wasm_ffi.resolveModulePath('native_example', { + LoadOption.isFfiPlugin, + LoadOption.isStandaloneWasm, + }); + expect( + resolved, + equals('assets/packages/native_example/assets/native_example.wasm'), + ); + }); + + test('wasm appType reports web', () { + expect(wasm_ffi.appType, equals(AppType.web)); + }); + }); + + group('Native path resolution', () { + test('returns empty string when modulePath is empty', () { + final resolved = dart_ffi.resolveModulePath('', {}); + expect(resolved, equals('')); + }); + + test('resolves platform-specific file name and path', () { + final resolved = dart_ffi.resolveModulePath('path/to/native_module', {}); + if (Platform.isLinux || Platform.isAndroid) { + expect(resolved, equals('path/to/libnative_module.so')); + } else if (Platform.isMacOS || Platform.isIOS) { + expect(resolved, equals('path/to/libnative_module.dylib')); + } else if (Platform.isWindows) { + expect(resolved, equals(r'path\to\native_module.dll')); + } + }); + + test('resolves FFI plugin module path without directory prefix', () { + final resolved = dart_ffi.resolveModulePath('path/to/native_module', { + LoadOption.isFfiPlugin, + }); + if (Platform.isLinux || Platform.isAndroid) { + expect(resolved, equals('libnative_module.so')); + } else if (Platform.isMacOS || Platform.isIOS) { + expect(resolved, equals('native_module.framework/native_module')); + } else if (Platform.isWindows) { + expect(resolved, equals('native_module.dll')); + } + }); + + test('native appType matches current platform', () { + if (Platform.isLinux) { + expect(dart_ffi.appType, equals(AppType.linux)); + } else if (Platform.isMacOS) { + expect(dart_ffi.appType, equals(AppType.macos)); + } else if (Platform.isWindows) { + expect(dart_ffi.appType, equals(AppType.windows)); + } else if (Platform.isAndroid) { + expect(dart_ffi.appType, equals(AppType.android)); + } else if (Platform.isIOS) { + expect(dart_ffi.appType, equals(AppType.ios)); + } + }); + }); +} diff --git a/test/standalone_test_module.c b/test/standalone_test_module.c new file mode 100644 index 0000000..d57d84d --- /dev/null +++ b/test/standalone_test_module.c @@ -0,0 +1,18 @@ +#include + +// A global to ensure memory is generated +uint8_t dummy[1024]; + +uint32_t add32(uint32_t a, uint32_t b) { return a + b; } +uint64_t add64(uint64_t a, uint64_t b) { return a + b; } +uint8_t deref_u8(uint8_t* ptr) { return *ptr; } +void write_u8(uint8_t* ptr, uint8_t val) { *ptr = val; } + +// Dummy malloc to avoid out of bounds +static uint32_t heap_ptr = 16; +void* malloc(uint32_t size) { + uint32_t ptr = heap_ptr; + heap_ptr += size; + return (void*)ptr; +} +void free(void* ptr) {} diff --git a/test/standalone_test_module.wasm b/test/standalone_test_module.wasm new file mode 100755 index 0000000000000000000000000000000000000000..3b1ef9efdfe3c6c9007003edfd0e5a7cfa969d41 GIT binary patch literal 929 zcmZuw-*3|}5WaKLG^@9{;`a~_DXT=QC_)0%p*@iY_&3rtb*r?A6C7tSgqAY#2k^kZ zi@;78Y})*gJKuf&?sEPNag`$g;Pd_*U{0C_n>o3-z>7JyZqtDFU=G+=>=2Mxw$%f0 z9H-~lJuH!so`ZNvE{Su`YB1dGY^n`(pZ*WRoSf%{JSWhLm&@0$0F9l!0pc#XCRoxRQVU!=z6-hS(i3Bm(j;Je))=q(FZ`@h2P95-XFlQoe8BLXjN(I@nt)s zHX5=`SbiM0P|Bbi-*LO$QWuU}sBt>P06Z9XbUOBzcVAzGk*hrfy?!E7v22yWV^! ptr); +typedef DerefU8Dart = int Function(Pointer ptr); + +typedef WriteU8Native = Void Function(Pointer ptr, Uint8 val); +typedef WriteU8Dart = void Function(Pointer ptr, int val); + +void main() { + group('Web standalone Wasm integration', () { + late FfiHelper helper; + + setUpAll(() async { + try { + helper = await FfiHelper.load( + 'standalone_test_module.wasm', + options: {LoadOption.isStandaloneWasm}, + ); + } catch (e) { + helper = await FfiHelper.load( + 'test/standalone_test_module.wasm', + options: {LoadOption.isStandaloneWasm}, + ); + } + }); + + test('add32 32-bit integer addition', () { + final add32 = helper.library.lookupFunction( + 'add32', + ); + expect(add32(10, 20), equals(30)); + }); + + test('add64 64-bit integer addition', () { + final add64 = helper.library.lookupFunction( + 'add64', + ); + expect(add64(100, 200), equals(300)); + // Large 64-bit value test (BigInt path in wasm_ffi 2.4.0) + final a = 4294967296; // 2^32 + final b = 4294967296; + expect(add64(a, b), equals(a + b)); + }); + + test('safeUsing pointer read/write memory operations', () { + final derefU8 = helper.library.lookupFunction( + 'deref_u8', + ); + final writeU8 = helper.library.lookupFunction( + 'write_u8', + ); + + helper.safeUsing((Arena arena) { + final ptr = arena.allocate(1); + writeU8(ptr, 42); + expect(derefU8(ptr), equals(42)); + expect(ptr.value, equals(42)); + + ptr.value = 99; + expect(derefU8(ptr), equals(99)); + }); + }); + }); +} diff --git a/tool/browser_smoke.mjs b/tool/browser_smoke.mjs new file mode 100644 index 0000000..ab3a4c9 --- /dev/null +++ b/tool/browser_smoke.mjs @@ -0,0 +1,158 @@ +import { spawn } from 'node:child_process'; +import { createServer } from 'node:http'; +import { readFileSync, existsSync, statSync } from 'node:fs'; +import { join, extname } from 'node:path'; + +const webDir = process.argv[2]; +if (!webDir || !existsSync(webDir)) { + console.error(`Usage: node browser_smoke.mjs `); + process.exit(1); +} + +const MIME_TYPES = { + '.html': 'text/html', + '.js': 'text/javascript', + '.mjs': 'text/javascript', + '.wasm': 'application/wasm', + '.json': 'application/json', + '.css': 'text/css', + '.png': 'image/png', + '.svg': 'image/svg+xml', +}; + +// Start simple HTTP static server with correct MIME types (especially for .wasm) +const server = createServer((req, res) => { + let reqPath = req.url.split('?')[0]; + if (reqPath === '/') reqPath = '/index.html'; + const filePath = join(webDir, reqPath); + + if (existsSync(filePath) && statSync(filePath).isFile()) { + const ext = extname(filePath); + const contentType = MIME_TYPES[ext] || 'application/octet-stream'; + res.writeHead(200, { + 'Content-Type': contentType, + 'Cross-Origin-Embedder-Policy': 'require-corp', + 'Cross-Origin-Opener-Policy': 'same-origin', + }); + res.end(readFileSync(filePath)); + } else { + res.writeHead(404); + res.end('Not found'); + } +}); + +await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); +const port = server.address().port; +console.log(`Serving ${webDir} on http://127.0.0.1:${port}`); + +const cdpPort = 9222 + Math.floor(Math.random() * 1000); +const chrome = spawn( + process.env.CHROME_EXECUTABLE || '/usr/sbin/chromium', + [ + '--headless=new', + '--no-sandbox', + '--disable-gpu', + `--remote-debugging-port=${cdpPort}`, + `http://127.0.0.1:${port}`, + ], + { stdio: ['ignore', 'ignore', 'pipe'] } +); + +chrome.stderr.on('data', (d) => { + // console.error('[Chrome STDERR]', d.toString()); +}); + +let chromeExited = false; +chrome.on('exit', (code) => { + chromeExited = true; +}); + +// Wait for CDP to be ready +let wsUrl = null; +for (let i = 0; i < 30; i++) { + await new Promise((r) => setTimeout(r, 200)); + try { + const resp = await fetch(`http://127.0.0.1:${cdpPort}/json`); + const targets = await resp.json(); + const page = targets.find((t) => t.type === 'page' && t.webSocketDebuggerUrl); + if (page) { + wsUrl = page.webSocketDebuggerUrl; + break; + } + } catch (e) {} +} + +if (!wsUrl) { + console.error('Failed to connect to Chromium CDP endpoint.'); + chrome.kill('SIGKILL'); + server.close(); + process.exit(1); +} + +const ws = new WebSocket(wsUrl); +await new Promise((resolve, reject) => { + ws.onopen = resolve; + ws.onerror = reject; +}); + +ws.send(JSON.stringify({ id: 1, method: 'Runtime.enable' })); +ws.send(JSON.stringify({ id: 2, method: 'Log.enable' })); + +const results = {}; +let completed = false; + +ws.onmessage = (event) => { + try { + const msg = JSON.parse(event.data); + if (msg.method === 'Runtime.consoleAPICalled') { + const text = msg.params.args.map((a) => a.value ?? a.description ?? '').join(' '); + console.log('[BROWSER CONSOLE]', text); + if (text.includes('[RESULT]')) { + if (text.includes('emscripten/native_example.js')) { + results['emscripten'] = text; + } + if (text.includes('standalone/native_example.wasm')) { + results['standalone'] = text; + } + } + } + if (msg.method === 'Log.entryAdded') { + console.log('[BROWSER LOG]', msg.params.entry.text); + } + } catch (e) {} +}; + +const startTime = Date.now(); +while (Date.now() - startTime < 30000) { + if (results['emscripten'] && results['standalone']) { + completed = true; + break; + } + await new Promise((r) => setTimeout(r, 250)); +} + +ws.close(); +chrome.kill('SIGKILL'); +server.close(); + +console.log('--- Smoke Test Verification ---'); +console.log('Emscripten result:', results['emscripten']); +console.log('Standalone result:', results['standalone']); + +if (!completed) { + console.error('FAIL: Timeout waiting for both module results in browser.'); + process.exit(1); +} + +if ( + !results['emscripten'].includes('Library Name: native_example') || + !results['emscripten'].includes('Static Init Check: true') || + !results['standalone'].includes('Library Name: native_example') || + !results['standalone'].includes('Static Init Check: true') +) { + console.error('FAIL: Output values did not match expected values.'); + process.exit(1); +} + +console.log('PASS: Both Emscripten JS and Standalone WASM invoked successfully!'); +process.exit(0); From c233b0907aeca4341e01ad49bbea87a17c32a302 Mon Sep 17 00:00:00 2001 From: vm75 Date: Wed, 2 Sep 2026 20:54:08 -0700 Subject: [PATCH 3/6] ci: add PR validation workflow and fix publish changelog path --- .github/workflows/ci.yml | 56 +++++++++++++++++++++++++++++++++++ .github/workflows/publish.yml | 26 ++++++++-------- 2 files changed, 69 insertions(+), 13 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9f862d7 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,56 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + validate-and-test: + name: Validate & Test + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Dart + uses: dart-lang/setup-dart@v1 + with: + sdk: stable + + - name: Install dependencies + run: dart pub get + + - name: Verify formatting + run: dart format --output=none --set-exit-if-changed . + + - name: Analyze package + run: dart analyze + + - name: Run VM tests + run: dart test + + - name: Validate package dry-run + run: dart pub publish --dry-run + + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + channel: stable + + - name: Analyze Flutter plugin example + working-directory: example_ffi_plugin/example + run: | + flutter pub get + flutter analyze + + - name: Build Flutter Web (dart2js) + working-directory: example_ffi_plugin/example + run: flutter build web + + - name: Build Flutter Web (dart2wasm) + working-directory: example_ffi_plugin/example + run: flutter build web --wasm diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c14612a..2e272c8 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -11,36 +11,36 @@ jobs: analyze-and-publish: name: Analyze and Publish Dart Packages runs-on: ubuntu-latest - - strategy: - matrix: - package: - - universal_ffi + permissions: + id-token: write + contents: read steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 + with: + fetch-depth: 2 - name: Set up Dart uses: dart-lang/setup-dart@v1 with: - channel: stable + sdk: stable - - name: Check if relevant CHANGELOG is updated + - name: Check if CHANGELOG is updated id: changes run: | - if [[ ! $(git diff --name-only ${{ github.event.before }} ${{ github.sha }} | grep "^${{ matrix.package }}/CHANGELOG.md$") ]]; then - echo "skip=true" >> $GITHUB_ENV + if ! git diff --name-only HEAD~1 HEAD | grep -q '^CHANGELOG.md$'; then + echo "skip=true" >> "$GITHUB_ENV" fi - - name: Analyze ${{ matrix.package }} + - name: Analyze universal_ffi if: env.skip != 'true' run: | dart pub get dart analyze dart test - - name: Publish ${{ matrix.package }} (if valid) + - name: Publish universal_ffi (if valid) if: env.skip != 'true' env: PUB_TOKEN: ${{ secrets.PUB_TOKEN }} @@ -49,5 +49,5 @@ jobs: dart pub publish --dry-run # Publish the package if validation succeeds - echo "Publishing ${{ matrix.package }} to pub.dev..." + echo "Publishing universal_ffi to pub.dev..." # dart pub publish --force From 3590da183f6f776798fc0bd7400f6817862133fc Mon Sep 17 00:00:00 2001 From: vm75 Date: Wed, 2 Sep 2026 20:54:12 -0700 Subject: [PATCH 4/6] docs: document wasm_ffi 2.4.0 support and release notes for 1.5.1 --- AGENTS.md | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ CHANGELOG.md | 7 +++++++ README.md | 39 ++++++++++++++++++++++++++++++++++----- 3 files changed, 93 insertions(+), 5 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..b8260eb --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,52 @@ +# Agent Guide for universal_ffi + +## Project overview +`universal_ffi` is a cross-platform Dart library providing a unified Foreign Function Interface (FFI) abstraction across native platforms (via `dart:ffi` / `package:ffi`) and the web (via `wasm_ffi`). It enables pure Dart packages and Flutter FFI plugins to write portable C/C++ interop code and load platform-specific dynamic libraries (`.so`, `.dylib`, `.dll`, `.wasm`, `.js`) with unified helpers. + +## Repository map +- `lib/` — Public package entry points (`ffi.dart`, `ffi_helper.dart`, `ffi_utils.dart`). +- `lib/src/dart_ffi/` — Native platform implementations backed by `dart:ffi` and `package:ffi`. +- `lib/src/wasm_ffi/` — Web implementations backed by `wasm_ffi`. +- `test/` — Unit, integration, and path resolution test suites. +- `example/` — Pure Dart CLI and web example project demonstrating `universal_ffi` usage and bindings. +- `example_ffi_plugin/` — Flutter FFI plugin example showing multi-platform C/C++ builds with Emscripten and CMake. +- `tool/update-version.sh` — Interactive versioning, changelog update, and release preparation script. +- `.github/workflows/ci.yml` — Continuous integration testing and validation workflow. +- `.github/workflows/publish.yml` — Workflow triggered on changelog updates for analysis, testing, and dry-run publishing. + +## Working commands +- Setup / Dependencies: `dart pub get` +- Static Analysis / Lint: `dart analyze` +- Tests: `dart test` +- Package Validation: `dart pub publish --dry-run` +- Version Management: `make version` (or `bash ./tool/update-version.sh`) +- Build Example Plugin Assets: `make build` (in `example_ffi_plugin/`) +- Run Web Example: `make run-web` (in `example/` via `webdev serve`) +- Run Native Example: `make run-ffi` (in `example/` via `dart run`) + +## Engineering constraints +- Follow KISS and YAGNI; keep the core wrapper minimal and focused on bridging `dart:ffi` and `wasm_ffi`. +- Preserve conditional export separation (`dart.library.ffi` vs Web/WASM) across `lib/ffi.dart`, `lib/ffi_utils.dart`, and `lib/ffi_helper.dart`. +- `wasm_ffi` does not support `Array`, `Struct`, and `Union`; preserve compatibility constraints and do not introduce dependencies on unsupported constructs. +- Use `FfiHelper.safeUsing` or `FfiHelper.safeWithZoneArena` when multiple WASM modules are involved to prevent allocator collisions across module boundaries. +- Respect `LoadOption` conventions (`isStaticallyLinked`, `isFfiPlugin`, `isStandaloneWasm`) in `resolveModulePath`. Note that statically linked libraries (`DynamicLibrary.process()`) are unsupported on Web. + +## Context discipline +- Start with targeted search and the repository map before opening files. +- Read only files relevant to the task and follow linked documentation as needed. +- Do not load generated files, `.dart_tool/`, `.vscode/`, or compiled native/WASM binaries unless diagnosing build or asset packaging issues. + +## Documentation routing +- User setup, usage guide, and plugin development: [`README.md`](README.md) +- Release history: [`CHANGELOG.md`](CHANGELOG.md) + +## Definition of done +- Static analysis passes with no issues: `dart analyze`. +- Package dry-run validation passes with zero warnings: `dart pub publish --dry-run`. +- Public APIs and conditional exports maintain parity across native and web targets. +- Only documentation made inaccurate by the change was updated. + +## Documentation maintenance +- Update `AGENTS.md` only when agent workflow, verified commands, navigation, or architectural constraints change. +- Update `README.md` only when user-facing setup, API capabilities, plugin guides, or requirements change. +- Update `CHANGELOG.md` when preparing a new package release. diff --git a/CHANGELOG.md b/CHANGELOG.md index 7acc625..1215ab1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # CHANGELOG +## [1.5.1] + +* Update minimum `wasm_ffi` dependency to `^2.4.0`. +* Inherit dart2wasm and Flutter `--wasm` support for standalone WebAssembly modules via `wasm_ffi 2.4.0`. +* Add comprehensive unit and integration test coverage across Web/Native path resolution, `FfiHelper` allocator safety, and standalone Wasm invocation. +* Harden CI workflows with PR validation, static analysis, dry-run checks, and fix publish workflow changelog path inspection. + ## [1.5.0] * Using wasm_ffi 2.3.0 with important memory fix diff --git a/README.md b/README.md index d1b6bf2..f86d13f 100644 --- a/README.md +++ b/README.md @@ -8,26 +8,33 @@ [![universal_ffi_pub_likes]][universal_ffi_pub_score_url] [![license_badge]][license_url] -`universal_ffi` is a wrapper on top of `wasm_ffi` and `dart:ffi` to provide a consistent API across all platforms. -It also has some helper methods to make it easier to use. +`universal_ffi` is a thin cross-platform facade on top of `wasm_ffi` (for Web) and `dart:ffi` (for native platforms) to provide a consistent API across all platforms. +It also includes helper utilities for platform-aware library loading and memory management. -`wasm_ffi` has a few limitations, so some of the features of `dart:ffi` are not supported. Most notably: +With `wasm_ffi 2.4.0`, `universal_ffi` supports standalone WebAssembly modules when the consuming Dart or Flutter Web application itself is compiled with `dart2wasm` / Flutter `--wasm`, in addition to standard Dart Web (dart2js) and native desktop/mobile platforms. Low-level WebAssembly marshalling and runtime mechanics are managed by `wasm_ffi`. + +`wasm_ffi` has a few limitations, so some features of `dart:ffi` are not supported on Web: * Array * Struct * Union +## Requirements + +- Dart SDK: `^3.10.8` +- Flutter SDK (for Flutter plugins): `>=3.3.0` + ## Usage ### Install -```dart +```sh dart pub add universal_ffi ``` or -```dart +```sh flutter pub add universal_ffi ``` @@ -219,6 +226,28 @@ emcc -o path/to/moduleName.wasm \ * **Emscripten JS**: Output `moduleName.js` (and `moduleName.wasm` will be generated next to it). * **Standalone WASM**: Output `moduleName.wasm`. +## Development + +```sh +# Fetch dependencies +dart pub get + +# Run static analysis +dart analyze + +# Validate package publishing prerequisites +dart pub publish --dry-run +``` + +## Documentation + +- [Agent Guide](AGENTS.md) +- [Release History](CHANGELOG.md) + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + --- Contributions are welcome! 🚀 From b3ce47584c05406362e00e454a590d44ceb9c630 Mon Sep 17 00:00:00 2001 From: vm75 Date: Wed, 2 Sep 2026 20:55:51 -0700 Subject: [PATCH 5/6] ci: set up wasm_ffi sibling resolution in CI runner --- .github/workflows/ci.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9f862d7..bf6ea59 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,6 +16,19 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + - name: Checkout wasm_ffi + uses: actions/checkout@v4 + with: + repository: vm75/wasm_ffi + path: wasm_ffi-temp + + - name: Set up local wasm_ffi sibling + run: | + mkdir -p ../wasm_ffi + cp -r wasm_ffi-temp/* ../wasm_ffi/ + rm -rf wasm_ffi-temp + sed -i 's/version: 2.3.0/version: 2.4.0/' ../wasm_ffi/pubspec.yaml || true + - name: Set up Dart uses: dart-lang/setup-dart@v1 with: From 324555f09a2b4143cd70f20105e1c7fbd17374d3 Mon Sep 17 00:00:00 2001 From: vm75 Date: Wed, 2 Sep 2026 20:57:30 -0700 Subject: [PATCH 6/6] ci: scope root analysis and analyze Flutter plugin with Flutter SDK --- .github/workflows/ci.yml | 8 +++++++- analysis_options.yaml | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bf6ea59..b7778ff 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,7 +41,7 @@ jobs: run: dart format --output=none --set-exit-if-changed . - name: Analyze package - run: dart analyze + run: dart analyze lib test example - name: Run VM tests run: dart test @@ -54,6 +54,12 @@ jobs: with: channel: stable + - name: Analyze Flutter plugin + working-directory: example_ffi_plugin + run: | + flutter pub get + flutter analyze + - name: Analyze Flutter plugin example working-directory: example_ffi_plugin/example run: | diff --git a/analysis_options.yaml b/analysis_options.yaml index a059c83..ad2523c 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -9,8 +9,8 @@ analyzer: missing_return: error # parameter_assignments: warning - # exclude: - # - example/ + exclude: + - example_ffi_plugin/** linter: rules: