nandi/vflutter_ffipublic Fork 0
4506a586d7400b00d732dcb97167be75ab41cab1
Commits
Clone
git clone https://git.rickub.com/nandi/vflutter_ffi.git
git clone ssh://git@rickub.com/nandi/vflutter_ffi.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

README.md · 237 lines · 10.3 KBmarkdown Blame HistoryRaw
Replace V with Nim 472eb49 nandithebull 21h ago1# nimflutter_ffi
Initial commit: V -> Flutter FFI plugin ad0ccda nandi yesterday2
Replace V with Nim 472eb49 nandithebull 21h ago3Write application logic in **Nim**, call it from **Flutter** over `dart:ffi`.
Initial commit: V -> Flutter FFI plugin ad0ccda nandi yesterday4
5Built on Flutter's `plugin_ffi` template. No platform channels, no method-channel
Replace V with Nim 472eb49 nandithebull 21h ago6serialisation — Dart calls Nim through the C ABI directly.
Initial commit: V -> Flutter FFI plugin ad0ccda nandi yesterday7
8## How it works
9
Replace V with Nim 472eb49 nandithebull 21h ago10Nim compiles to C. That is the whole trick:
Initial commit: V -> Flutter FFI plugin ad0ccda nandi yesterday11
12```
Replace V with Nim 472eb49 nandithebull 21h ago13src/nimflutter.nim --[ nim c --compileOnly ]--> C --[ NDK / clang / MSVC ]--> libnimflutter.so
Initial commit: V -> Flutter FFI plugin ad0ccda nandi yesterday14```
15
Replace V with Nim 472eb49 nandithebull 21h ago16Nim never cross-compiles for the target itself. It only emits C, and the
17platform's own toolchain owns the ABI, sysroot and flags. That is why Android
18arm64, iOS arm64, Linux, macOS, Windows and wasm32 all work from one source
19file.
20
21One wrinkle versus a single-file emitter: Nim writes one `.c` per module plus
22`nimbase.h`, and the set is only known after it runs. `src/CMakeLists.txt`
23therefore runs Nim at *configure* time and globs the result, rather than
24wiring an `add_custom_command`.
Initial commit: V -> Flutter FFI plugin ad0ccda nandi yesterday25
26| Platform | Built by | Artifact |
27|---|---|---|
Replace V with Nim 472eb49 nandithebull 21h ago28| Android | NDK via `android/build.gradle``src/CMakeLists.txt` | `libnimflutter.so` per ABI |
Initial commit: V -> Flutter FFI plugin ad0ccda nandi yesterday29| Linux / Windows | Flutter's CMake → `src/CMakeLists.txt` | shared library, auto-bundled |
30| iOS / macOS | CocoaPods compiles pre-generated C | static archive in the app binary |
31
Replace V with Nim 472eb49 nandithebull 21h ago32iOS is the odd one out: Xcode's build sandbox can't run `nim`, and App Store
Initial commit: V -> Flutter FFI plugin ad0ccda nandi yesterday33builds want a static archive. So `tool/gen_ios_sources.sh` generates the C ahead
34of time into `ios/Classes/`, and that is checked in. **Re-run it whenever
Replace V with Nim 472eb49 nandithebull 21h ago35`src/nimflutter.nim` changes.**
Initial commit: V -> Flutter FFI plugin ad0ccda nandi yesterday36
37## The two rules
38
39Everything that goes wrong with this bridge goes wrong in one of two ways.
40
41**1. Only C types cross the boundary.**
Replace V with Nim 472eb49 nandithebull 21h ago42`cint`, `cdouble`, `cstring`, `pointer`. Never a Nim `string`, `seq`, `ref` or
43object — those have Nim-specific layouts and lifetimes Dart cannot read.
44Convert at the edge.
45
46**2. Memory that crosses the boundary is manually owned, and comes from the
47shared heap.**
48
49The library is built `--mm:arc`. ARC frees Nim's own temporaries
50deterministically at scope exit, so the leak-every-temporary problem does not
51arise — but anything handed *out* to Dart has deliberately escaped that, and
52the caller must return it:
53
54```nim
55proc nf_greet(name: cstring): cstring {.exportc: "nf_greet", dynlib, cdecl.} =
56 let greeting = "Hello, " & $name & ", from Nim!" # ARC frees this at exit
57 # allocShared0, not alloc0: Dart may free this from a different isolate than
58 # the one that allocated it, and Nim's default heap is thread-local.
59 let buf = cast[cstring](allocShared0(greeting.len + 1))
60 copyMem(buf, greeting.cstring, greeting.len)
61 buf # caller owns this
Initial commit: V -> Flutter FFI plugin ad0ccda nandi yesterday62```
63
Replace V with Nim 472eb49 nandithebull 21h ago64`--mm:arc` rather than the default ORC because a C-ABI surface has no cycles
65to collect, and no reason to pay for a cycle detector.
66
67Ownership across the boundary: **Nim allocates, Dart copies, Nim frees.** The
68Dart wrapper in `lib/nimflutter_ffi.dart` does the `nf_free` in a `finally`, so
69callers never hold a pointer and never leak. `tool/../example/lib/memory_check.dart`
70holds RSS flat across 900k round trips.
Initial commit: V -> Flutter FFI plugin ad0ccda nandi yesterday71
Replace V with Nim 472eb49 nandithebull 21h ago72Better still, for bulk data, don't allocate at all: let Dart own the buffer and
73have Nim fill it in place. `nf_mandelbrot` works that way, which is why it
74needs no `nf_free` — and why it takes a `buf_len` it validates in 64-bit before
75writing a single byte.
Initial commit: V -> Flutter FFI plugin ad0ccda nandi yesterday76
Add a justfile f7c3825 nandithebull 23h ago77## Quick start
78
79With [`just`](https://github.com/casey/just):
80
81```
Replace V with Nim 472eb49 nandithebull 21h ago82just mandelbrot # build the Nim library and run the fractal explorer
83just web # serve the same app with Nim as WebAssembly (no browser opened)
Add a justfile f7c3825 nandithebull 23h ago84just test # analyze + the example's test suite
Replace V with Nim 472eb49 nandithebull 21h ago85just bench # Nim vs Dart timings for the same frame
Add a justfile f7c3825 nandithebull 23h ago86just --list # everything else
87```
88
Replace V with Nim 472eb49 nandithebull 21h ago89Flutter is expected at `~/flutter/bin` and Nim at `~/nim/bin`; set
90`FLUTTER_BIN` or `NIM_BIN` if yours live elsewhere.
Add a justfile f7c3825 nandithebull 23h ago91
Initial commit: V -> Flutter FFI plugin ad0ccda nandi yesterday92## Usage
93
94```dart
Replace V with Nim 472eb49 nandithebull 21h ago95import 'package:nimflutter_ffi/nimflutter_ffi.dart' as v;
Initial commit: V -> Flutter FFI plugin ad0ccda nandi yesterday96
Add a WebAssembly backend for the example 728acaa nandithebull 22h ago97await v.initialize(); // required on web, a formality on native
Initial commit: V -> Flutter FFI plugin ad0ccda nandi yesterday98v.add(20, 22); // 42
Replace V with Nim 472eb49 nandithebull 21h ago99v.greet('Flutter'); // "Hello, Flutter, from Nim!"
Initial commit: V -> Flutter FFI plugin ad0ccda nandi yesterday100await v.greetAsync('isolate'); // same, off the UI isolate
101```
102
Replace V with Nim 472eb49 nandithebull 21h ago103ARC has no stop-the-world phase, so calls are safe from any isolate — provided
104anything handed across the boundary comes from the shared heap, as above. Use
105`Isolate.run` for anything long enough to jank a frame.
Initial commit: V -> Flutter FFI plugin ad0ccda nandi yesterday106
Replace V with Nim 472eb49 nandithebull 21h ago107For bulk data, let Dart own the buffer and have Nim fill it in place — then
Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull yesterday108nothing crosses the boundary that needs freeing, and independent slices can be
109computed concurrently:
110
111```dart
112final view = v.FractalView(width: 800, height: 600, maxIter: 500);
113final pixels = await v.renderParallel(view, tiles: 4); // RGBA8888
114```
115
Add a WebAssembly backend for the example 728acaa nandithebull 22h ago116## Web
117
Replace V with Nim 472eb49 nandithebull 21h ago118The same Nim source also runs in the browser. `just web` compiles it to
Add a WebAssembly backend for the example 728acaa nandithebull 22h ago119WebAssembly with emscripten and serves the example app at
120`http://localhost:8080` — it prints the URL and waits rather than launching a
121browser, so open it yourself (`WEB_PORT=9000 just web` to move it). Hot
122restart still works. `dart:ffi` does not exist on web, so a second backend
123drives the wasm module over `dart:js_interop` instead:
124
125```
126lib/src/backend_native.dart dart:ffi
127lib/src/backend_web.dart dart:js_interop + emscripten
Replace V with Nim 472eb49 nandithebull 21h ago128lib/nimflutter_ffi.dart picks one with a conditional import
Add a WebAssembly backend for the example 728acaa nandithebull 22h ago129```
130
131Nothing has to be installed for this. `tool/bin/emcc` is a
132[DotSlash](https://dotslash-cli.com) file that pins emscripten by content
133hash; the toolchain is fetched on first use and cached in `~/.cache/dotslash`.
134Only `dotslash` itself needs to be on `PATH`.
135
136The two backends are held to being the same, not merely similar: `just parity`
137renders a frame with each kernel and compares them byte for byte, and they
138agree exactly — across different compilers, different libm implementations
139(glibc vs musl) and different word sizes (64-bit vs wasm32). Speed is close
140too, ~2-5% off native for the same frame:
141
142```
Replace V with Nim 472eb49 nandithebull 21h ago143800x600 maxIter 100: 68.3 ms native / 70.2 ms wasm
144800x600 maxIter 500: 148.8 ms native / 157.2 ms wasm
Add a WebAssembly backend for the example 728acaa nandithebull 22h ago145```
146
Render in parallel on web, via a pool of Web Workers 4506a58 nandithebull 21h ago147Web renders in parallel too, without `SharedArrayBuffer` or COOP/COEP
148headers: instead of threading one module, the pool runs *several instances* of
149it, one per Web Worker, each with its own linear memory. Nothing is shared, so
150there is nothing to synchronise, and a module is only ~34 KB. Measured in
151headless Chrome on 8 cores, 800x600 at 500 iterations:
152
153```
154main thread, one band 345 ms
1551 worker 164 ms
15632 bands over 8 workers 37 ms (4.5x)
157```
158
159More bands than workers, for the same reason as native: band costs are wildly
160uneven, so over-decomposing keeps a free worker busy rather than waiting on
161the slowest one. `just check-pool` re-measures this and asserts the pooled
162frame is byte-identical to a single-instance render — a pool that quietly
163falls back to one thread, or assembles bands out of order, otherwise looks
164just like a working one.
165
166One difference remains, and it is surfaced in the API:
Add a WebAssembly backend for the example 728acaa nandithebull 22h ago167
168* `await v.initialize()` before anything else. A wasm module cannot be
169 instantiated synchronously. On native it resolves immediately.
170
171### One trap worth knowing about
172
Replace V with Nim 472eb49 nandithebull 21h ago173Nim's `vmemcpy` silently skips any copy whose source or destination is
Add a WebAssembly backend for the example 728acaa nandithebull 22h ago174`<= 0xFFFF` — a null-pointer heuristic that is sound on 64-bit desktop, where
175the low 64 KB is never mapped. On wasm32 emscripten packs static data down at
Replace V with Nim 472eb49 nandithebull 21h ago176address ~1300, so every copy out of a string literal does nothing and Nim strings
Add a WebAssembly backend for the example 728acaa nandithebull 22h ago177come back as runs of zero bytes, with no crash and no diagnostic. It only
178appears at `-O1` and above, which makes it look like an optimiser bug.
179`tool/build_wasm.sh` passes `-sGLOBAL_BASE=1048576` to move static data, the
180stack and the heap clear of that check.
181
Replace V with Nim 472eb49 nandithebull 21h ago182## Is Nim faster than Dart?
Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull yesterday183
184For the numeric kernel in the example: **no, not meaningfully.** Measured at
Replace V with Nim 472eb49 nandithebull 21h ago185800x600 / 500 iterations on an 8-core Linux box, Nim through C takes ~152 ms
186against Dart AOT's ~169 ms — about 1.1x, which is inside the range where the
187answer depends on the loop rather than the language. Dart's AOT compiler is
188good at scalar floating-point loops. (V, which this plugin used previously,
189measured the same ~1.1x.)
190
191What the bridge does buy you is the ability to *write the logic in Nim* — or
192reuse Nim you already have — with a C ABI that is cheap to call, safe to call
193concurrently, and that also compiles to wasm for the web build. Pick it for the
194language, not for an expected speedup. And build with optimisation on: at
195`-O0`, or without `-d:danger`, the same kernel is several times slower, which
196is what `tool/build.sh` and `src/CMakeLists.txt` guard against.
197
198The parallel figures in the example come from splitting the frame across
199isolates, not from the language — see `just sweep`, which shows the best band
200count is well above the core count because the bands cost wildly different
201amounts.
Initial commit: V -> Flutter FFI plugin ad0ccda nandi yesterday202
203## Status
204
Replace V with Nim 472eb49 nandithebull 21h ago205Verified on Linux with Nim 2.2.0, Dart 3 and Flutter 3.47: host build, string
Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull yesterday206round-trip, isolate dispatch, 900k-call memory stability, and the example app
207built and run as a release Linux binary (CMake -> `v` -> NDK/clang path
Add a WebAssembly backend for the example 728acaa nandithebull 22h ago208included).
209
210The web path is verified end to end as well — wasm built with emscripten 6.0.9,
211the module exercised headlessly under node, the kernel compared byte for byte
212against the native build, and the release web app loaded in headless Chrome
213with its rendered canvas checked for an actual fractal. `just ci` runs all of
214it.
215
216The Android/iOS/Windows glue is written to the standard `plugin_ffi` contract
217but is not exercised here — it needs the respective toolchains.
Initial commit: V -> Flutter FFI plugin ad0ccda nandi yesterday218
219## Layout
220
221```
Replace V with Nim 472eb49 nandithebull 21h ago222src/nimflutter.v the Nim source — the only file you normally edit
223src/nimflutter.h C declarations (ffigen input, Xcode input)
224src/CMakeLists.txt Nim -> C -> shared lib; shared by Android/Linux/Windows
225lib/nimflutter_ffi.dart the Dart API callers use
Add a WebAssembly backend for the example 728acaa nandithebull 22h ago226lib/src/backend_*.dart the two backends: dart:ffi and wasm/js_interop
Initial commit: V -> Flutter FFI plugin ad0ccda nandi yesterday227ios/, macos/ pre-generated C + podspec (static archive)
228android/build.gradle NDK build via externalNativeBuild
229tool/build.sh host build + export smoke test
Replace V with Nim 472eb49 nandithebull 21h ago230tool/gen_ios_sources.sh regenerate ios/ and macos/ C after editing the Nim source
Add a WebAssembly backend for the example 728acaa nandithebull 22h ago231tool/bin/emcc DotSlash file pinning emscripten — nothing to install
Replace V with Nim 472eb49 nandithebull 21h ago232tool/build_wasm.sh Nim -> C -> wasm for the web build
Add a WebAssembly backend for the example 728acaa nandithebull 22h ago233tool/test_wasm.mjs headless check of the wasm module (no browser needed)
234tool/check_parity.sh native vs wasm kernel, byte for byte
235tool/check_web.sh loads the built web app in headless Chrome
Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull yesterday236example/ Flutter app exercising the bridge (see example/README.md)
Initial commit: V -> Flutter FFI plugin ad0ccda nandi yesterday237```