| Initial commit: V -> Flutter FFI plugin ad0ccda nandi 6h 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 | |
| 63 | ## Usage |
| 64 | |
| 65 | ```dart |
| 66 | import 'package:vflutter_ffi/vflutter_ffi.dart' as v; |
| 67 | |
| 68 | v.add(20, 22); // 42 |
| 69 | v.greet('Flutter'); // "Hello, Flutter, from V!" |
| 70 | await v.greetAsync('isolate'); // same, off the UI isolate |
| 71 | ``` |
| 72 | |
| 73 | `-gc none` means no stop-the-world phase and no thread-local runtime state, so |
| 74 | calls are safe from any isolate. Use `Isolate.run` for anything long enough to |
| 75 | jank a frame. |
| 76 | |
| 77 | ## Adding a function |
| 78 | |
| 79 | 1. Export it in `src/vflutter.v` with `@[export: 'vf_yourthing']`, freeing temporaries. |
| 80 | 2. Declare it in `src/vflutter.h`. |
| 81 | 3. Wrap it in `lib/vflutter_ffi.dart`. |
| 82 | 4. `./tool/gen_ios_sources.sh` to refresh the iOS C. |
| 83 | 5. `./tool/build.sh` to smoke-test the host build. |
| 84 | |
| 85 | Step 2 also feeds `dart run ffigen --config ffigen.yaml` if you'd rather |
| 86 | generate the raw bindings than hand-write them. |
| 87 | |
| 88 | ## Status |
| 89 | |
| 90 | Verified on Linux with V 0.5.2 + Dart 3: build, string round-trip, isolate |
| 91 | dispatch, and 900k-call memory stability. The Android/iOS/Windows glue is |
| 92 | written to the standard `plugin_ffi` contract but is not exercised here — |
| 93 | it needs a Flutter SDK and the respective toolchains. |
| 94 | |
| 95 | ## Layout |
| 96 | |
| 97 | ``` |
| 98 | src/vflutter.v the V source — the only file you normally edit |
| 99 | src/vflutter.h C declarations (ffigen input, Xcode input) |
| 100 | src/CMakeLists.txt V -> C -> shared lib; shared by Android/Linux/Windows |
| 101 | lib/vflutter_ffi.dart the Dart API callers use |
| 102 | ios/, macos/ pre-generated C + podspec (static archive) |
| 103 | android/build.gradle NDK build via externalNativeBuild |
| 104 | tool/build.sh host build + export smoke test |
| 105 | tool/gen_ios_sources.sh regenerate ios/ and macos/ C after editing the V source |
| 106 | ``` |