# vflutter_ffi Write application logic in **V**, call it from **Flutter** over `dart:ffi`. Built on Flutter's `plugin_ffi` template. No platform channels, no method-channel serialisation — Dart calls V through the C ABI directly. ## How it works V compiles to C. That is the whole trick: ``` src/vflutter.v --[ v -shared -gc none ]--> C --[ NDK / clang / MSVC ]--> libvflutter.so ``` V never cross-compiles for the target. It only emits C, and the platform's own toolchain owns the ABI, sysroot and flags. That is why Android arm64, iOS arm64, Linux, macOS and Windows all work from one source file. | Platform | Built by | Artifact | |---|---|---| | Android | NDK via `android/build.gradle` → `src/CMakeLists.txt` | `libvflutter.so` per ABI | | Linux / Windows | Flutter's CMake → `src/CMakeLists.txt` | shared library, auto-bundled | | iOS / macOS | CocoaPods compiles pre-generated C | static archive in the app binary | iOS is the odd one out: Xcode's build sandbox can't run `v`, and App Store builds want a static archive. So `tool/gen_ios_sources.sh` generates the C ahead of time into `ios/Classes/`, and that is checked in. **Re-run it whenever `src/vflutter.v` changes.** ## The two rules Everything that goes wrong with this bridge goes wrong in one of two ways. **1. Only C types cross the boundary.** `int`, `f64`, `&char`, `voidptr`. Never a V string, array, map, option or sumtype — those have V-specific layouts Dart cannot read. Convert at the edge. **2. The library is built `-gc none`, so V code must free its own temporaries.** This is the one that bites. Boehm GC can't be cross-compiled per-ABI without pain, and a C-ABI library with explicit ownership doesn't need it — but it means *every* intermediate allocation inside an exported function leaks unless freed: ```v @[export: 'vf_greet'] fn vf_greet(name &char) &char { n := unsafe { cstring_to_vstring(name) } // allocates res := 'Hello, ${n}, from V!' // allocates out := unsafe { res.str } // handed to the caller unsafe { n.free() } // <-- without this, ~40 bytes/call return out } ``` Measured on this repo: omitting `n.free()` leaks ~12 MB per 300k calls. With it, RSS is flat at 300k and 900k calls. Ownership across the boundary: **V allocates, Dart copies, V frees.** The Dart wrapper in `lib/vflutter_ffi.dart` does the `vf_free` in a `finally`, so callers never hold a pointer and never leak. ## Usage ```dart import 'package:vflutter_ffi/vflutter_ffi.dart' as v; v.add(20, 22); // 42 v.greet('Flutter'); // "Hello, Flutter, from V!" await v.greetAsync('isolate'); // same, off the UI isolate ``` `-gc none` means no stop-the-world phase and no thread-local runtime state, so calls are safe from any isolate. Use `Isolate.run` for anything long enough to jank a frame. ## Adding a function 1. Export it in `src/vflutter.v` with `@[export: 'vf_yourthing']`, freeing temporaries. 2. Declare it in `src/vflutter.h`. 3. Wrap it in `lib/vflutter_ffi.dart`. 4. `./tool/gen_ios_sources.sh` to refresh the iOS C. 5. `./tool/build.sh` to smoke-test the host build. Step 2 also feeds `dart run ffigen --config ffigen.yaml` if you'd rather generate the raw bindings than hand-write them. ## Status Verified on Linux with V 0.5.2 + Dart 3: build, string round-trip, isolate dispatch, and 900k-call memory stability. The Android/iOS/Windows glue is written to the standard `plugin_ffi` contract but is not exercised here — it needs a Flutter SDK and the respective toolchains. ## Layout ``` src/vflutter.v the V source — the only file you normally edit src/vflutter.h C declarations (ffigen input, Xcode input) src/CMakeLists.txt V -> C -> shared lib; shared by Android/Linux/Windows lib/vflutter_ffi.dart the Dart API callers use ios/, macos/ pre-generated C + podspec (static archive) android/build.gradle NDK build via externalNativeBuild tool/build.sh host build + export smoke test tool/gen_ios_sources.sh regenerate ios/ and macos/ C after editing the V source ```