Skip to content

Swift Binding Conventions

Resources:

The Swift binding uses Swift’s C importer over the public C headers. Keep the importer as an implementation dependency and expose one low-level Swift package product. The binding adapts ownership, diagnostics, callbacks, copied values, and render targets; adapters own scheduling, SDK, rendering, and app policy. Use one public Swift target for handles, descriptors, events, errors, and backend interop values, with status conversion, diagnostics, descriptor materializers, handle state, callbacks, and copied-result helpers kept as internal implementation files in that target.

Do not export C importer symbols. Raw pointers, imported C structs, field masks, function pointers, and @convention(c) trampolines stay internal. Public modules group C API concepts.

Long-lived native objects are final classes with the shared Handle suffix. Final classes give ARC stable identity and avoid value copies of native owners. Each class stores the native pointer, release state, parent references, callbacks, and optional leak context.

Swift value types model copied C data. Descriptors are structs unless identity or shared mutation is useful. Descriptor APIs set semantic fields; internal materializers write C size fields, masks, arrays, string views, and nested descriptors. Field-mask domains use optional stored properties, explicit clear...() methods, or a small presence type. Use one style per descriptor.

Closed C enum domains map to Swift enums with explicit raw conversion helpers. Output domains that may grow preserve unknown raw values, for example case unknown(UInt32). C bit masks become OptionSet types. Native result and list handles stay internal; readers copy them into Swift values before release.

NativePointer is a borrowed opaque address value. Store the address as a private UInt or equivalent bit-pattern value and expose no memory access. Convert to UnsafeRawPointer? or UnsafeMutableRawPointer? only inside support code for APIs whose C contract accepts opaque backend handles.

Handle release is explicit and fallible. Public handle classes provide close() throws. A successful close calls the C destroy function, releases Swift-owned adapter state, marks the wrapper closed, and makes later close() calls no-ops. If native destruction fails, close() throws and leaves the native pointer live so callers can retry on the correct owner thread.

deinit reports leaks. It may release only native resources whose C release function is documented as thread-independent and infallible. Thread-affine resources rely on explicit close() because ARC can finalize on an arbitrary thread or after parents have gone away.

Child handles retain their parent wrapper while native validity depends on it: maps retain runtimes, render sessions retain maps, and style-scoped state retains its owner. MapProjectionHandle is the exception: it owns a standalone projection snapshot and does not retain the source MapHandle for native validity.

Public fallible methods use throws. Map each native status to a stable MaplibreError case carrying the raw status and copied diagnostic when present. Status checks read the diagnostic immediately after the failing C call on the same thread. The binding validates Swift-owned state: closed wrappers, active borrows, invalid strings, completed requests, and callbacks. The C API validates native arguments, owner threads, ranges, and MapLibre rules.

Swift 6 concurrency annotations describe the contract. Owner-thread handles are non-Sendable; immutable copied values may conform to Sendable. Callback boxes become Sendable only when their closure and captures can run from MapLibre worker, network, logging, or render-related threads. The binding does not dispatch or add @MainActor to low-level APIs. Native wrong-thread statuses become Swift errors.

Materialize imported C inputs at the call boundary. Use stack variables, withUnsafePointer, withUnsafeBytes, withUnsafeMutableBytes, temporary arrays, and scoped UTF-8 storage. Pointers derived from Swift String, Array, or Data live only inside the withUnsafe... closure unless the binding copies them into owned native storage.

Null-terminated string inputs reject embedded NUL. Explicit-length mln_string_view inputs use UTF-8 bytes plus byte length. Copied byte payloads use Data when Foundation is already part of the package surface or [UInt8] for a pure standard-library shape. Use one shape within each concept. CPU readback APIs should support caller-owned mutable storage and may offer a copied image convenience path.

Borrowed native output becomes copied Swift data before the borrow window ends. Internal guards release native snapshot, result, and list handles in all paths. Runtime polling returns independent Swift events. Session-owned texture frames expose backend handles through a frame-scoped view or closure. The view checks that the explicit frame handle is still live before converting to NativePointer; plain NativePointer values from a frame do not escape the scope.

Callbacks use noncapturing @convention(c) trampolines. Store Swift callback state in a retained box and pass it through C user_data with Unmanaged: registration retains with passRetained, trampolines recover with fromOpaque(...).takeUnretainedValue(), and teardown releases exactly once with takeRetainedValue() or an equivalent owner object. The box owns the closure, active-upcall counters, and native registration tokens for the C owner scope. Mutable box state uses locks, atomics, or another explicit synchronization primitive when callbacks may arrive concurrently.

Trampolines copy or scope callback arguments before invoking Swift. They catch Swift errors and convert failures to the documented C behavior; Swift errors do not escape through C frames. If a callback API can throw, the adapter records diagnostics or returns a C failure status.

Callbacks may arrive on MapLibre worker, network, logging, or render-related threads. Public callback types use @Sendable where the closure can run cross-thread. Use @unchecked Sendable only for callback boxes whose synchronization invariants are documented and enforced. Resource provider callbacks copy request fields before user code can retain them. A handled request object owns the native request reference, enforces one-shot completion, and releases exactly once. Custom geometry callbacks track active upcalls and delay state release until in-flight callbacks finish. When replacing a callback, install the new native descriptor before releasing the old Swift box.

Render target descriptors are Swift values. Surface and borrowed-texture descriptors store host-owned backend handles as NativePointer; callers keep those backend objects valid and synchronized for the C API’s required lifetime. Attach methods return RenderSessionHandle, one attached target for one map.

Session-owned texture targets use explicit frame handle classes with close() throws. Frame handles keep the session live, release the native frame once, and reject access after close. Safe accessors return copied metadata; backend interop uses a frame-scoped view or closure and documents unsafe synchronization requirements.

Swift tests exercise the public Swift API against the real C library. Focus on throwing status conversion, diagnostic copying, explicit close behavior, non-Sendable owner-thread handles, callback box synchronization, copied event payloads, and frame-scope invalidation.