Dart binding
maplibre_native_ffi is the low-level Dart binding for the public MapLibre
Native C API. The package exposes explicit native handle lifetimes, copied value
types, runtime event batches, resource callbacks, offline operations, and the
render backend descriptors used by host integrations.
Build and test
The repository-level mise configuration installs Dart and the native build toolchain. From the repository root:
mise run //bindings/dart:test
mise run //bindings/dart:test linux-x64-vulkan
mise run //bindings/dart:build:mobile android-arm64-egl
mise run //bindings/dart:build:mobile ios-arm64-metal
mise run //bindings/dart:build:mobile ios-simulator-arm64-metal
mise run --force //bindings/dart:ffigen
The test task builds the selected CMake preset, points the build hook at the
resulting install prefix, analyzes the package, and runs the Dart tests. The
private raw declarations are checked in so Git and pub package consumers receive
a complete library; CI regenerates them and fails on any diff. Generation is
configured in tool/ffigen.dart.
The mobile build task creates a temporary Flutter host, builds the selected native preset, and verifies that Flutter packages its code asset. Device and simulator iOS use separate presets because their dynamic libraries target different Apple SDKs.
The native library reaches Dart as a code asset that hook/build.dart declares,
which is how the generated @Native declarations resolve it. Build hooks run in
a semi-hermetic environment that strips arbitrary environment variables, so the
hook reads the install prefix from .dart_tool/maplibre_native_install_dir
rather than from an environment variable; the mise tasks write it. Without that
file the hook downloads the artifact matching the target from the snapshot
release, which is what a consumer taking this package as a git dependency gets.
Dart runs the hook for dart run as well as dart test, so regenerating the
bindings resolves a library it never calls. Run the test task first and the
pointer already names a local build; on its own,
mise run //bindings/dart:ffigen downloads one.
Android host integration
The code asset carries the library on Android as it does everywhere else, so an Android application packages the ABI it points the hook at and the platform loader is never asked for one by name. What Android needs on top of the library is the patched Rustls platform-verifier helper that the native TLS stack calls over JNI, without which HTTPS requests cannot validate against the platform trust policy:
mise run //bindings/rustls-platform-verifier-android:build
The application binds nothing in the helper, which uses a MapLibre FFI-private
Java package so it can coexist with another library that packages the upstream
Rustls helper, and the AAR carries the R8 keep rule the helper needs. The host
calls mln_android_init with its JNI environment, class, and application
context before creating a runtime.
The Kotlin runtime AARs bundle that helper together with
libmaplibre-native-c.so for each ABI. A Dart host packages the library itself,
so it takes the helper AAR alone rather than a runtime AAR carrying a second
copy of the same library.
Ownership and execution
Owned handles have an idempotent close() or discard() operation. Close child
maps, render sessions, frames, snapshots, request handles, and offline
operations before their parent runtime. Scoped backend values remain valid only
until their frame or owner is closed.
Runtime and map work is synchronous and owner-thread-affine. Keep a handle and
all calls that use it on the isolate that created it. Run queued callbacks with
RuntimeHandle.pump(), then take the events it produced with
RuntimeHandle.drainEvents(). Narrow what a map or a runtime queues with
setEventMask.
A render session is the exception: it belongs to the isolate that attached it,
which need not be the map's. A MapHandle cannot cross isolates, so
MapHandle.attachRef() produces a MapAttachRef that can. It carries the
native address and attaches; every other map call stays on the map's isolate.
Known draft deviation: do not await in an isolate that holds a handle
The C API keys owner-thread checks on the OS thread. This binding keys them on
Isolate.current.hashCode, and the two are not equivalent: the Dart VM moves an
isolate between OS threads, and it does so when an isolate resumes from awaited
I/O. The isolate hash does not change, so the binding's own check still passes
while the native check starts failing.
Until that is addressed, do not await I/O on an isolate that holds a runtime,
map, projection, or render session. Create the handles, use them, and close them
without yielding to I/O in between. Dart offers no equivalent of Go's
runtime.LockOSThread(), so the binding cannot pin the isolate on your behalf.
Exceeding this produces wrongThread from every call on the handle, including
close(). Because close fails too, the native runtime is never destroyed and
mln_runtime_destroy refuses for the rest of the process.
Tracked in #412.
Resource-request completion is one-shot. Calling complete() or close()
releases the provider reference even when completion reports a native error.
Callback exceptions are contained at the native boundary and reported through
the native diagnostic path.
Unsigned C uint64_t JSON values, feature identifiers, and camera transition
IDs use Dart BigInt so the complete native range is preserved. Native buffers
return copied bytes; direct pointer access is explicitly unsafe and ends at
NativeBuffer.close().
Libraries
- maplibre_native_ffi
- Low-level Dart bindings for the MapLibre Native C API.
- src/camera/camera
- Camera descriptor and map camera operation types.
- src/error/maplibre_exception
- src/geo/geo
- Geographic, geometry, tile, and feature value types.
- src/internal/c/maplibre_native_c
- src/internal/c/maplibre_native_c.g
- src/internal/callback/callback_state
- src/internal/lifecycle/frame_construction
- src/internal/lifecycle/lifecycle
- src/internal/lifecycle/native_handles
- src/internal/memory/memory
- src/internal/status/status
- src/internal/struct/struct
- src/internal/value/byte_values
- src/internal/value/uint64
- src/log/log
- Log configuration and copied native log records.
- src/map/map
- src/maplibre
- src/offline/offline
- Offline region definitions, status values, metadata, and operation handles.
- src/projection/projection
- Map projection handle and coordinate projection helper APIs.
- src/query/query
- Rendered and source feature query descriptors and copied query hits.
- src/render/native_pointer
- src/render/render
- Render session, render target, texture frame, and readback APIs.
- src/render/targets
- src/resource/resource
- Resource requests, responses, transforms, providers, and request handles.
- src/runtime/runtime
- src/style/style
- Style source, layer, image, light, property, and custom geometry APIs.