| Initial commit: V -> Flutter FFI plugin ad0ccda nandi 4h ago | 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 4m 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 |
| 69 | just test # analyze + the example's test suite |
| 70 | just bench # V vs Dart timings for the same frame |
| 71 | just --list # everything else |
| 72 | ``` |
| 73 | |
| 74 | Flutter is expected at `~/flutter/bin`; set `FLUTTER_BIN` if yours lives |
| 75 | elsewhere. |
| 76 | |
| Initial commit: V -> Flutter FFI plugin ad0ccda nandi 4h ago | 77 | ## Usage |
| 78 | |
| 79 | ```dart |
| 80 | import 'package:vflutter_ffi/vflutter_ffi.dart' as v; |
| 81 | |
| 82 | v.add(20, 22); // 42 |
| 83 | v.greet('Flutter'); // "Hello, Flutter, from V!" |
| 84 | await v.greetAsync('isolate'); // same, off the UI isolate |
| 85 | ``` |
| 86 | |
| 87 | `-gc none` means no stop-the-world phase and no thread-local runtime state, so |
| 88 | calls are safe from any isolate. Use `Isolate.run` for anything long enough to |
| 89 | jank a frame. |
| 90 | |
| Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 12m ago | 91 | For bulk data, let Dart own the buffer and have V fill it in place — then |
| 92 | nothing crosses the boundary that needs freeing, and independent slices can be |
| 93 | computed concurrently: |
| 94 | |
| 95 | ```dart |
| 96 | final view = v.FractalView(width: 800, height: 600, maxIter: 500); |
| 97 | final pixels = await v.renderParallel(view, tiles: 4); // RGBA8888 |
| 98 | ``` |
| 99 | |
| 100 | ## Is V faster than Dart? |
| 101 | |
| 102 | For the numeric kernel in the example: **no, not meaningfully.** Measured at |
| 103 | 800x600 / 500 iterations on an 8-core Linux box, V through C takes ~152 ms |
| 104 | against Dart AOT's ~165 ms — inside the noise of a ~10% margin. Dart's AOT |
| 105 | compiler is good at scalar floating-point loops. |
| 106 | |
| 107 | What the bridge does buy you is the ability to *write the logic in V* — or |
| 108 | reuse V you already have — with a C ABI that is cheap to call and safe to call |
| 109 | concurrently. Pick it for the language, not for an expected speedup. And build |
| 110 | with optimisation on: at `-O0` the same kernel is roughly 2x slower than Dart, |
| 111 | which is what `tool/build.sh` and `src/CMakeLists.txt` now guard against. |
| 112 | |
| Initial commit: V -> Flutter FFI plugin ad0ccda nandi 4h ago | 113 | ## Adding a function |
| 114 | |
| 115 | 1. Export it in `src/vflutter.v` with `@[export: 'vf_yourthing']`, freeing temporaries. |
| 116 | 2. Declare it in `src/vflutter.h`. |
| 117 | 3. Wrap it in `lib/vflutter_ffi.dart`. |
| 118 | 4. `./tool/gen_ios_sources.sh` to refresh the iOS C. |
| 119 | 5. `./tool/build.sh` to smoke-test the host build. |
| 120 | |
| 121 | Step 2 also feeds `dart run ffigen --config ffigen.yaml` if you'd rather |
| 122 | generate the raw bindings than hand-write them. |
| 123 | |
| 124 | ## Status |
| 125 | |
| Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 12m ago | 126 | Verified on Linux with V 0.5.2, Dart 3 and Flutter 3.47: host build, string |
| 127 | round-trip, isolate dispatch, 900k-call memory stability, and the example app |
| 128 | built and run as a release Linux binary (CMake -> `v` -> NDK/clang path |
| 129 | included). The Android/iOS/Windows glue is written to the standard `plugin_ffi` |
| 130 | contract but is not exercised here — it needs the respective toolchains. |
| Initial commit: V -> Flutter FFI plugin ad0ccda nandi 4h ago | 131 | |
| 132 | ## Layout |
| 133 | |
| 134 | ``` |
| 135 | src/vflutter.v the V source — the only file you normally edit |
| 136 | src/vflutter.h C declarations (ffigen input, Xcode input) |
| 137 | src/CMakeLists.txt V -> C -> shared lib; shared by Android/Linux/Windows |
| 138 | lib/vflutter_ffi.dart the Dart API callers use |
| 139 | ios/, macos/ pre-generated C + podspec (static archive) |
| 140 | android/build.gradle NDK build via externalNativeBuild |
| 141 | tool/build.sh host build + export smoke test |
| 142 | tool/gen_ios_sources.sh regenerate ios/ and macos/ C after editing the V source |
| Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 12m ago | 143 | example/ Flutter app exercising the bridge (see example/README.md) |
| Initial commit: V -> Flutter FFI plugin ad0ccda nandi 4h ago | 144 | ``` |