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:
@[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
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
- Export it in
src/vflutter.vwith@[export: 'vf_yourthing'], freeing temporaries. - Declare it in
src/vflutter.h. - Wrap it in
lib/vflutter_ffi.dart. ./tool/gen_ios_sources.shto refresh the iOS C../tool/build.shto 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
|