Guidelines
Follow these guidelines when you change the public API or documentation.
If a rule doesn’t fit a case, change the rule in the same PR instead of making an exception. When a tool enforces a rule, delete it here.
Versioning
Section titled “Versioning”The native library and every binding share one version number,
EPOCH.YYYYMM.REVISION. YYYYMM is the UTC year and month of the release.
REVISION numbers the releases in that month, starting at 0.
The epoch says what stays compatible. In epoch 0, nothing does: make breaking
changes when they improve the API, and don’t add compatibility shims. In epoch 1
and later, a release doesn’t break the API, ABI, or documented behavior of
earlier releases with the same epoch, in the library or in any binding. A
breaking change increments the epoch. MLN_C_VERSION and mln_c_version()
equal the epoch.
The public API is the C headers in include/. A generator in tools/bindgen
reads them and writes each language binding’s declarations.
Besides the C types, the generator reads MLN_BINDING annotations. They say who
owns each value, how long it lives, whether it can be null, and how each
function runs. The generator uses them to write safe wrappers in each binding.
Naming
Section titled “Naming”- Name a function
mln_<owner>_<verb>[_<noun>], where the owner is the type of its first parameter:mln_map_get_style_layertakes anmln_map. A function without a handle parameter starts with its subsystem instead:mln_log_set_callback. - Use
get_for a function that reads one value,list_for one that reads all of them, andis_for a predicate. - Name a bitmask enum
mln_<x>_flag, and the type that holds a set of its bitsmln_<x>_flags. - Prefer the MapLibre style spec’s terms over engine terms.
- Camera terms from the style spec keep their spec names and units:
bearing,pitch,roll, andfield_of_vieware in degrees, andcenter_altitudeis in meters. Their comments state the unit. - A
_sizeis a count of bytes, and a_countis a count of items. - When another plain number’s type doesn’t show its unit, end its name with the
unit:
duration_ms,expires_unix_ms.
General API design
Section titled “General API design”-
A function that returns an error status must not change any state, except an answer to a request, such as
mln_resource_request_complete. An answer ends its handle whatever it returns, and carriesMLN_BINDING("consumes=always"). -
An asynchronous function takes an
mln_completion, which receives its result. -
Annotate each function with how it runs, as in
MLN_BINDING("execution=command"):Execution Function commandChanges state asynchronously queryReads state asynchronously operationDoes other asynchronous work lifecycleCreates, attaches, or retires a handle asynchronously immediateReturns its result synchronously; the default snapshotCopies the current state behind a handle synchronously event_batchTakes the queued frame results as a batch, or a null batch -
Fix surprising behavior instead of documenting it. When a contract is hard to state, change the API instead of writing more prose.
Handles
Section titled “Handles”A handle is an opaque 64-bit ID for state that native owns and keeps between
calls, such as a runtime, a map, a parsed style expression, or a delivery queue.
Callers can copy and compare handles and pass them between threads, but can’t
see the state behind a handle, and must not create or decode one.
MLN_HANDLE_NULL is the null handle. Native never reuses a released handle.
typedef uint64_t mln_style_expression MLN_BINDING( "kind=handle;release=mln_style_expression_release");typedef uint64_t mln_map MLN_BINDING( "kind=handle;release=mln_map_release;dispose=mln_map_dispose");
// A create function writes the handle to an output that must hold the null// handle on entry.mln_style_expression expression = MLN_HANDLE_NULL;mln_style_expression_create(json, NULL, &expression, NULL);mln_style_expression_release(expression);Most handles are freed right away, so they have one teardown function,
void _release(handle). Some handles can’t be freed until other work finishes,
such as a runtime that has to wait for its maps to retire. These have two:
_release(handle, completion)is for an orderly shutdown. It rejects a handle that isn’t ready, such as a runtime that still has maps, and the completion runs once teardown finishes._dispose(handle)is for destructors and finalizers, which can’t wait or handle errors. It accepts the handle in any state and finishes teardown in the background.
Every binding names these release and dispose in its own case, such as
ReleaseAsync and Dispose in .NET, and has no other name for either. Its
scoped cleanup calls one of them: a scope that can wait without blocking a
thread, such as await using, calls the release, and every other scope, such as
a destructor or Python’s with, calls the dispose.
Types that may grow
Section titled “Types that may grow”In epoch 1 and later, a release can add to the API but can’t break existing declarations. Code built against older headers must keep working with a newer library, so each type is declared in a way that lets later releases extend it.
typedef enum mln_status : int32_t { MLN_STATUS_OK = 0, MLN_STATUS_INVALID_ARGUMENT = -1, MLN_STATUS_INVALID_STATE = -2, // A later release adds -3, -4, and so on.} mln_status;
typedef struct mln_diagnostic { uint32_t size; // sizeof(mln_diagnostic) in the caller's headers char message[MLN_DIAGNOSTIC_MESSAGE_CAPACITY]; // A later release adds members here.} mln_diagnostic;
typedef struct mln_lat_lng { // No size: this type never changes. double latitude; double longitude;} mln_lat_lng;- Use fixed-width integers,
size_t,float,double, andbool. - An enum declares a fixed underlying type, usually
uint32_t. A later release can add values, so code that reads an enum from native must accept values its headers don’t name. - A struct passed by pointer begins with
uint32_t size, and a later release adds members only at its end. The side that allocates the struct setssizeto itssizeof, and the other side reads or writes only the members within that size. - A struct that is only ever embedded by value in another struct has no
size. Its container’ssizeversions it. - A struct that is also passed by pointer on its own keeps its
sizewhen another struct embeds it, asmln_camera_updateembedsmln_camera_options. Native ignores the embeddedsize. - A small value type that is not intended to grow, such as a coordinate or an
ID, does not need
size.
Option bags
Section titled “Option bags”An options struct holds a function’s settings. Each optional setting has a bit
in the struct’s fields member. Native reads a setting only when its bit is
set. For a struct that configures one call, native uses the setting’s default
otherwise, so a struct with only size set selects every default.
typedef enum mln_style_expression_option_field : uint32_t { MLN_STYLE_EXPRESSION_OPTION_EXPECTED_TYPE = 1U << 0U,} mln_style_expression_option_field;
typedef struct mln_style_expression_options { uint32_t size; mln_style_expression_option_fields fields; mln_style_expression_type expected_type MLN_BINDING("mask=fields;bit=MLN_STYLE_EXPRESSION_OPTION_EXPECTED_TYPE");} mln_style_expression_options;
mln_style_expression_options options = {.size = sizeof(options)};options.fields = MLN_STYLE_EXPRESSION_OPTION_EXPECTED_TYPE;options.expected_type.kind = MLN_STYLE_EXPRESSION_TYPE_NUMBER;- The bits are a bitmask enum named
mln_<x>_field, andfieldshas the typemln_<x>_fields. - A member with no sensible default, such as a width or a Vulkan device, is
required instead. It has no bit and carries
MLN_BINDING("required=true"). - A struct that updates state a handle keeps, such as the one
mln_map_set_viewport_optionstakes, changes only the settings whose bits are set. The others keep their current values instead of returning to defaults, and the function’s comment says so. - A function that reads back the settings of such a struct reports a record that
holds every setting, without
sizeorfields. The record is named for what it holds, without_options:mln_cameraformln_camera_options. A function that writes the record through a pointer wraps it in a struct that begins withsize, asmln_map_projection_camera_resultwraps anmln_camera.
Data in and out
Section titled “Data in and out”Neither side frees memory that the other allocated. Data passed into a function is borrowed for the call. Data that native returns is either written into storage that the caller provides, or lent by native until a stated point, such as when a callback returns.
// Borrowed for the call: the caller can free json once it returns.mln_style_expression_create(json, NULL, &expression, NULL);
// Written into the caller's buffer. The first call reports the length.size_t length = 0;mln_style_expression_evaluate(expression, NULL, NULL, 0, &length, NULL);char* result = malloc(length + 1);mln_style_expression_evaluate( expression, NULL, result, length + 1, &length, NULL);
// Lent until the callback returns, so the caller copies what it keeps.void on_complete(void* user_data, const mln_completion_result* result) { keep_copy(result->diagnostic);}- Pass text as UTF-8 in an
mln_buffer_view, not as a NUL-terminated string. - A function that keeps an input copies it before it returns.
- A synchronous function that returns data of a length the caller can’t know writes it into a buffer the caller passes, and always reports the full length, so the caller can call again with enough room.
- Lent data stays valid until a point that the declaration states, such as when a callback returns or a handle is released.
Callbacks
Section titled “Callbacks”Native calls caller code through callbacks, to deliver a result, report an
event, or ask the caller for data. A callback comes in a registration struct,
which pairs it with user_data, the caller’s context pointer, and with a
function that frees that context. Native runs the callback on a thread that it
picks, unless the registration names an mln_delivery_queue. Then native queues
each call, and the callback runs when the caller dispatches the queue, on a
thread the caller chooses.
typedef struct mln_log_handler { uint32_t size; mln_log_callback callback; void* user_data; mln_user_data_release release_user_data; mln_delivery_queue queue; mln_log_severity_flags platform_severities;} mln_log_handler;
mln_status mln_log_set_callback( const mln_log_handler* handler, mln_diagnostic* out_diagnostic);- Native calls
release_user_dataonce, after the last call to the callback. - Every binding presents a registration struct as a type of its own, even one that holds a single callback, so the struct can gain members without breaking callers.
- Callbacks should accept a delivery queue unless native needs the callback’s result before it can continue.
- Some language bindings may not expose callbacks without a delivery queue, or may expose that functionality with an alternative declarative api.
Platform-specific APIs
Section titled “Platform-specific APIs”A function that only some platforms can use lists those platforms in its
annotation. A function that integrates with one platform’s own objects, such as
its windows or display timing, goes in that platform’s header under
include/maplibre_native_c/platform/.
MLN_BINDING("execution=lifecycle;platforms=ios,macos,tvos,visionos")MLN_API mln_status mln_map_attach_metal_surface(/* ... */);Documentation
Section titled “Documentation”Use plain English, without idioms, Latin abbreviations, or jargon. Assume the reader is lightly familiar with MapLibre, but not with its internals.
Docs site pages introduce concepts and show common tasks, not every feature or detailed contracts. Avoid bloating these; not every feature needs a section and example on the docs site. Avoid narrating implementation details.
Guide pages share their prose across languages, so put language-specific code in
snippets. To name an API symbol in prose, write its C name as
[](api:mln_map_set_style_url). The site shows the name in the reader’s
language and links to that language’s reference.
An API reference exists for each language binding; the C reference is the source, and the rest are generated from there. The API reference describes observable functionality and contracts; it must not narrate implementation.
Inline comments in the code explain constraints that the code can’t show, such as from a dependency, platform, vendor, or protocol that we can’t change. If the surprising behavior is rooted in our own code’s design, rethink the code instead.
API reference comments
Section titled “API reference comments”The C header comments are the API reference for every binding. Write them for someone integrating the library: what a declaration does, and what a caller must know to use it correctly. Promise only that. Behavior that holds today but that an integration doesn’t need stays out of the comment, so we can change it.
A comment has a one-sentence summary, then paragraphs about behavior, then these sections in order. Leave out a section that would be empty:
Threads:says where a callback runs, when its caller needs to know.Returns:lists the statuses a correct caller handles, each with its condition.Completes with:does the same for the completion.Lifetimes:says how long each pointer stays valid and who ends it.
Give every function and type a summary. Comment a field or an enum constant only when its name and type don’t say what a caller needs.
Name parameters in backticks. Leave out what bindings take over: completions,
out_diagnostic, output pointers, size fields, and a callback’s user_data,
release function, and queue. The rules above cover them, and anything more that
a C caller needs about them goes under Lifetimes:.
The generator copies each comment into every binding, with C names and
parameters spelled as that binding’s symbols. It turns Returns: and
Completes with: into the binding’s errors, and leaves Lifetimes: in C.
/** * Removes a source from the map's style. * * Completes with: * - MLN_STATUS_NOT_FOUND when the style has no source named `source_id`. * - MLN_STATUS_INVALID_STATE when a layer still uses the source. */MLN_BINDING("execution=command")MLN_API mln_status mln_map_remove_style_source( mln_map map, mln_buffer_view source_id, const mln_completion* completion, mln_diagnostic* out_diagnostic) MLN_NOEXCEPT;
/** * Sets the headers of an HTTP request before the library sends it. * * `url` is the request's URL after any resource transform. The request waits * for the callback, so return quickly. * * Threads: * Runs on a library thread. * * Lifetimes: * `url` and `out_response` stay valid until the callback returns. */MLN_BINDING("failure=MLN_STATUS_NATIVE_ERROR")typedef mln_status (*mln_http_header_transform_callback)( void* user_data, mln_resource_kind kind, mln_buffer_view url, mln_http_header_transform_response* out_response MLN_BINDING("direction=out"));