nandi/vflutter_ffipublic Fork 0
59a731a766d2baec2d522605425d2fce9c6c7209
Commits
Clone
git clone https://git.rickub.com/nandi/vflutter_ffi.git
git clone ssh://git@rickub.com/nandi/vflutter_ffi.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Add a justfile f7c3825 · on 59a731a766d2baec2d522605425d2fce9c6c7209 · nandithebull · 23h ago
README.md · 144 lines · 5.6 KBmarkdown
Blame HistoryOpen raw

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.gradlesrc/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.

Quick start

With just:

just mandelbrot   # build the V library and run the fractal explorer
just test         # analyze + the example's test suite
just bench        # V vs Dart timings for the same frame
just --list       # everything else

Flutter is expected at ~/flutter/bin; set FLUTTER_BIN if yours lives
elsewhere.

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.

For bulk data, let Dart own the buffer and have V fill it in place — then
nothing crosses the boundary that needs freeing, and independent slices can be
computed concurrently:

final view = v.FractalView(width: 800, height: 600, maxIter: 500);
final pixels = await v.renderParallel(view, tiles: 4);   // RGBA8888

Is V faster than Dart?

For the numeric kernel in the example: no, not meaningfully. Measured at
800x600 / 500 iterations on an 8-core Linux box, V through C takes ~152 ms
against Dart AOT's ~165 ms — inside the noise of a ~10% margin. Dart's AOT
compiler is good at scalar floating-point loops.

What the bridge does buy you is the ability to write the logic in V — or
reuse V you already have — with a C ABI that is cheap to call and safe to call
concurrently. Pick it for the language, not for an expected speedup. And build
with optimisation on: at -O0 the same kernel is roughly 2x slower than Dart,
which is what tool/build.sh and src/CMakeLists.txt now guard against.

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 and Flutter 3.47: host build, string
round-trip, isolate dispatch, 900k-call memory stability, and the example app
built and run as a release Linux binary (CMake -> v -> NDK/clang path
included). The Android/iOS/Windows glue is written to the standard plugin_ffi
contract but is not exercised here — it needs 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
example/                Flutter app exercising the bridge (see example/README.md)
  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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# 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.

## Quick start

With [`just`](https://github.com/casey/just):

```
just mandelbrot   # build the V library and run the fractal explorer
just test         # analyze + the example's test suite
just bench        # V vs Dart timings for the same frame
just --list       # everything else
```

Flutter is expected at `~/flutter/bin`; set `FLUTTER_BIN` if yours lives
elsewhere.

## 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.

For bulk data, let Dart own the buffer and have V fill it in place — then
nothing crosses the boundary that needs freeing, and independent slices can be
computed concurrently:

```dart
final view = v.FractalView(width: 800, height: 600, maxIter: 500);
final pixels = await v.renderParallel(view, tiles: 4);   // RGBA8888
```

## Is V faster than Dart?

For the numeric kernel in the example: **no, not meaningfully.** Measured at
800x600 / 500 iterations on an 8-core Linux box, V through C takes ~152 ms
against Dart AOT's ~165 ms — inside the noise of a ~10% margin. Dart's AOT
compiler is good at scalar floating-point loops.

What the bridge does buy you is the ability to *write the logic in V* — or
reuse V you already have — with a C ABI that is cheap to call and safe to call
concurrently. Pick it for the language, not for an expected speedup. And build
with optimisation on: at `-O0` the same kernel is roughly 2x slower than Dart,
which is what `tool/build.sh` and `src/CMakeLists.txt` now guard against.

## 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 and Flutter 3.47: host build, string
round-trip, isolate dispatch, 900k-call memory stability, and the example app
built and run as a release Linux binary (CMake -> `v` -> NDK/clang path
included). The Android/iOS/Windows glue is written to the standard `plugin_ffi`
contract but is not exercised here — it needs 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
example/                Flutter app exercising the bridge (see example/README.md)
```