| Initial commit: V -> Flutter FFI plugin ad0ccda nandi yesterday | 1 | # vflutter_ffi |
| 2 | |
| 3 | Write application logic in **V**, call it from **Flutter** over `dart:ffi`. |
| 4 | |
| 5 | Built on Flutter's `plugin_ffi` template. No platform channels, no method-channel |
| 6 | serialisation — Dart calls V through the C ABI directly. |
| 7 | |
| 8 | ## How it works |
| 9 | |
| 10 | V compiles to C. That is the whole trick: |
| 11 | |
| 12 | ``` |
| 13 | src/vflutter.v --[ v -shared -gc none ]--> C --[ NDK / clang / MSVC ]--> libvflutter.so |
| 14 | ``` |
| 15 | |
| 16 | V never cross-compiles for the target. It only emits C, and the platform's own |
| 17 | toolchain owns the ABI, sysroot and flags. That is why Android arm64, iOS |
| 18 | arm64, Linux, macOS and Windows all work from one source file. |
| 19 | |
| 20 | | Platform | Built by | Artifact | |
| 21 | |---|---|---| |
| 22 | | Android | NDK via `android/build.gradle` → `src/CMakeLists.txt` | `libvflutter.so` per ABI | |
| 23 | | Linux / Windows | Flutter's CMake → `src/CMakeLists.txt` | shared library, auto-bundled | |
| 24 | | iOS / macOS | CocoaPods compiles pre-generated C | static archive in the app binary | |
| 25 | |
| 26 | iOS is the odd one out: Xcode's build sandbox can't run `v`, and App Store |
| 27 | builds want a static archive. So `tool/gen_ios_sources.sh` generates the C ahead |
| 28 | of time into `ios/Classes/`, and that is checked in. **Re-run it whenever |
| 29 | `src/vflutter.v` changes.** |
| 30 | |
| 31 | ## The two rules |
| 32 | |
| 33 | Everything that goes wrong with this bridge goes wrong in one of two ways. |
| 34 | |
| 35 | **1. Only C types cross the boundary.** |
| 36 | `int`, `f64`, `&char`, `voidptr`. Never a V string, array, map, option or |
| 37 | sumtype — those have V-specific layouts Dart cannot read. Convert at the edge. |
| 38 | |
| 39 | **2. The library is built `-gc none`, so V code must free its own temporaries.** |
| 40 | |
| 41 | This is the one that bites. Boehm GC can't be cross-compiled per-ABI without |
| 42 | pain, and a C-ABI library with explicit ownership doesn't need it — but it means |
| 43 | *every* intermediate allocation inside an exported function leaks unless freed: |
| 44 | |
| 45 | ```v |
| 46 | @[export: 'vf_greet'] |
| 47 | fn vf_greet(name &char) &char { |
| 48 | n := unsafe { cstring_to_vstring(name) } // allocates |
| 49 | res := 'Hello, ${n}, from V!' // allocates |
| 50 | out := unsafe { res.str } // handed to the caller |
| 51 | unsafe { n.free() } // <-- without this, ~40 bytes/call |
| 52 | return out |
| 53 | } |
| 54 | ``` |
| 55 | |
| 56 | Measured on this repo: omitting `n.free()` leaks ~12 MB per 300k calls. With it, |
| 57 | RSS is flat at 300k and 900k calls. |
| 58 | |
| 59 | Ownership across the boundary: **V allocates, Dart copies, V frees.** The Dart |
| 60 | wrapper in `lib/vflutter_ffi.dart` does the `vf_free` in a `finally`, so callers |
| 61 | never hold a pointer and never leak. |
| 62 | |
| Add a justfile f7c3825 nandithebull 20h ago | 63 | ## Quick start |
| 64 | |
| 65 | With [`just`](https://github.com/casey/just): |
| 66 | |
| 67 | ``` |
| 68 | just mandelbrot # build the V library and run the fractal explorer |
| Add a WebAssembly backend for the example 728acaa nandithebull 18h ago | 69 | just web # serve the same app with V as WebAssembly (no browser opened) |
| Add a justfile f7c3825 nandithebull 20h ago | 70 | just test # analyze + the example's test suite |
| 71 | just bench # V vs Dart timings for the same frame |
| 72 | just --list # everything else |
| 73 | ``` |
| 74 | |
| 75 | Flutter is expected at `~/flutter/bin`; set `FLUTTER_BIN` if yours lives |
| 76 | elsewhere. |
| 77 | |
| Initial commit: V -> Flutter FFI plugin ad0ccda nandi yesterday | 78 | ## Usage |
| 79 | |
| 80 | ```dart |
| 81 | import 'package:vflutter_ffi/vflutter_ffi.dart' as v; |
| 82 | |
| Add a WebAssembly backend for the example 728acaa nandithebull 18h ago | 83 | await v.initialize(); // required on web, a formality on native |
| Initial commit: V -> Flutter FFI plugin ad0ccda nandi yesterday | 84 | v.add(20, 22); // 42 |
| 85 | v.greet('Flutter'); // "Hello, Flutter, from V!" |
| 86 | await v.greetAsync('isolate'); // same, off the UI isolate |
| 87 | ``` |
| 88 | |
| 89 | `-gc none` means no stop-the-world phase and no thread-local runtime state, so |
| 90 | calls are safe from any isolate. Use `Isolate.run` for anything long enough to |
| 91 | jank a frame. |
| 92 | |
| Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 20h ago | 93 | For bulk data, let Dart own the buffer and have V fill it in place — then |
| 94 | nothing crosses the boundary that needs freeing, and independent slices can be |
| 95 | computed concurrently: |
| 96 | |
| 97 | ```dart |
| 98 | final view = v.FractalView(width: 800, height: 600, maxIter: 500); |
| 99 | final pixels = await v.renderParallel(view, tiles: 4); // RGBA8888 |
| 100 | ``` |
| 101 | |
| Add a WebAssembly backend for the example 728acaa nandithebull 18h ago | 102 | ## Web |
| 103 | |
| 104 | The same V source also runs in the browser. `just web` compiles it to |
| 105 | WebAssembly with emscripten and serves the example app at |
| 106 | `http://localhost:8080` — it prints the URL and waits rather than launching a |
| 107 | browser, so open it yourself (`WEB_PORT=9000 just web` to move it). Hot |
| 108 | restart still works. `dart:ffi` does not exist on web, so a second backend |
| 109 | drives the wasm module over `dart:js_interop` instead: |
| 110 | |
| 111 | ``` |
| 112 | lib/src/backend_native.dart dart:ffi |
| 113 | lib/src/backend_web.dart dart:js_interop + emscripten |
| 114 | lib/vflutter_ffi.dart picks one with a conditional import |
| 115 | ``` |
| 116 | |
| 117 | Nothing has to be installed for this. `tool/bin/emcc` is a |
| 118 | [DotSlash](https://dotslash-cli.com) file that pins emscripten by content |
| 119 | hash; the toolchain is fetched on first use and cached in `~/.cache/dotslash`. |
| 120 | Only `dotslash` itself needs to be on `PATH`. |
| 121 | |
| 122 | The two backends are held to being the same, not merely similar: `just parity` |
| 123 | renders a frame with each kernel and compares them byte for byte, and they |
| 124 | agree exactly — across different compilers, different libm implementations |
| 125 | (glibc vs musl) and different word sizes (64-bit vs wasm32). Speed is close |
| 126 | too, ~2-5% off native for the same frame: |
| 127 | |
| 128 | ``` |
| 129 | 800x600 maxIter 100: 68.1 ms native / 69.4 ms wasm |
| 130 | 800x600 maxIter 500: 149.3 ms native / 156.3 ms wasm |
| 131 | ``` |
| 132 | |
| 133 | Two differences are real and surfaced in the API: |
| 134 | |
| 135 | * `await v.initialize()` before anything else. A wasm module cannot be |
| 136 | instantiated synchronously. On native it resolves immediately. |
| 137 | * `v.supportsIsolateParallelism` is false on web. There are no isolates, so |
| 138 | `renderParallel` still splits the frame and still produces identical pixels, |
| 139 | but the bands run in sequence. |
| 140 | |
| 141 | ### One trap worth knowing about |
| 142 | |
| 143 | V's `vmemcpy` silently skips any copy whose source or destination is |
| 144 | `<= 0xFFFF` — a null-pointer heuristic that is sound on 64-bit desktop, where |
| 145 | the low 64 KB is never mapped. On wasm32 emscripten packs static data down at |
| 146 | address ~1300, so every copy out of a string literal does nothing and V strings |
| 147 | come back as runs of zero bytes, with no crash and no diagnostic. It only |
| 148 | appears at `-O1` and above, which makes it look like an optimiser bug. |
| 149 | `tool/build_wasm.sh` passes `-sGLOBAL_BASE=1048576` to move static data, the |
| 150 | stack and the heap clear of that check. |
| 151 | |
| Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 20h ago | 152 | ## Is V faster than Dart? |
| 153 | |
| 154 | For the numeric kernel in the example: **no, not meaningfully.** Measured at |
| 155 | 800x600 / 500 iterations on an 8-core Linux box, V through C takes ~152 ms |
| 156 | against Dart AOT's ~165 ms — inside the noise of a ~10% margin. Dart's AOT |
| 157 | compiler is good at scalar floating-point loops. |
| 158 | |
| 159 | What the bridge does buy you is the ability to *write the logic in V* — or |
| 160 | reuse V you already have — with a C ABI that is cheap to call and safe to call |
| 161 | concurrently. Pick it for the language, not for an expected speedup. And build |
| 162 | with optimisation on: at `-O0` the same kernel is roughly 2x slower than Dart, |
| 163 | which is what `tool/build.sh` and `src/CMakeLists.txt` now guard against. |
| 164 | |
| Initial commit: V -> Flutter FFI plugin ad0ccda nandi yesterday | 165 | ## Adding a function |
| 166 | |
| 167 | 1. Export it in `src/vflutter.v` with `@[export: 'vf_yourthing']`, freeing temporaries. |
| 168 | 2. Declare it in `src/vflutter.h`. |
| 169 | 3. Wrap it in `lib/vflutter_ffi.dart`. |
| 170 | 4. `./tool/gen_ios_sources.sh` to refresh the iOS C. |
| 171 | 5. `./tool/build.sh` to smoke-test the host build. |
| 172 | |
| 173 | Step 2 also feeds `dart run ffigen --config ffigen.yaml` if you'd rather |
| 174 | generate the raw bindings than hand-write them. |
| 175 | |
| 176 | ## Status |
| 177 | |
| Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 20h ago | 178 | Verified on Linux with V 0.5.2, Dart 3 and Flutter 3.47: host build, string |
| 179 | round-trip, isolate dispatch, 900k-call memory stability, and the example app |
| 180 | built and run as a release Linux binary (CMake -> `v` -> NDK/clang path |
| Add a WebAssembly backend for the example 728acaa nandithebull 18h ago | 181 | included). |
| 182 | |
| 183 | The web path is verified end to end as well — wasm built with emscripten 6.0.9, |
| 184 | the module exercised headlessly under node, the kernel compared byte for byte |
| 185 | against the native build, and the release web app loaded in headless Chrome |
| 186 | with its rendered canvas checked for an actual fractal. `just ci` runs all of |
| 187 | it. |
| 188 | |
| 189 | The Android/iOS/Windows glue is written to the standard `plugin_ffi` contract |
| 190 | but is not exercised here — it needs the respective toolchains. |
| Initial commit: V -> Flutter FFI plugin ad0ccda nandi yesterday | 191 | |
| 192 | ## Layout |
| 193 | |
| 194 | ``` |
| 195 | src/vflutter.v the V source — the only file you normally edit |
| 196 | src/vflutter.h C declarations (ffigen input, Xcode input) |
| 197 | src/CMakeLists.txt V -> C -> shared lib; shared by Android/Linux/Windows |
| 198 | lib/vflutter_ffi.dart the Dart API callers use |
| Add a WebAssembly backend for the example 728acaa nandithebull 18h ago | 199 | lib/src/backend_*.dart the two backends: dart:ffi and wasm/js_interop |
| Initial commit: V -> Flutter FFI plugin ad0ccda nandi yesterday | 200 | ios/, macos/ pre-generated C + podspec (static archive) |
| 201 | android/build.gradle NDK build via externalNativeBuild |
| 202 | tool/build.sh host build + export smoke test |
| 203 | tool/gen_ios_sources.sh regenerate ios/ and macos/ C after editing the V source |
| Add a WebAssembly backend for the example 728acaa nandithebull 18h ago | 204 | tool/bin/emcc DotSlash file pinning emscripten — nothing to install |
| 205 | tool/build_wasm.sh V -> C -> wasm for the web build |
| 206 | tool/test_wasm.mjs headless check of the wasm module (no browser needed) |
| 207 | tool/check_parity.sh native vs wasm kernel, byte for byte |
| 208 | tool/check_web.sh loads the built web app in headless Chrome |
| Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 20h ago | 209 | example/ Flutter app exercising the bridge (see example/README.md) |
| Initial commit: V -> Flutter FFI plugin ad0ccda nandi yesterday | 210 | ``` |