Skip to main content

Writing an extension

The SDK does not need to contain every format. A data source it has no factory for can live in its own library, be written in Kotlin or Swift, and still be a first-class facade object that specs and layers point at.

That is deliberate. GDAL is the example everyone reaches for — it would add tens of megabytes, a proj.db, four Android cross-builds and five iOS slices to a map SDK that most apps use for tiles. As an extension it costs the SDK nothing and costs an app only what it asks for.

The reference extension

massif-maps/gdal-extension reads GeoTIFF and OGR vector files this way, and doubles as a worked example of cross-building a minimal GDAL.

How it works

Three SDK pieces, all of which already exist:

TileDataSource and VectorDataSource are SWIG directorsso a binding can subclass them and override loadTile / loadElements
TileData takes a BinaryDataso an override can return bytes it decoded itself
MassifInterop.adopt(kind, id, source)registers the instance under a facade id, so specs and layers can reach it

An extension is therefore an ordinary Android or iOS library. It ships whatever native code it needs, and the SDK never links against it.

A tile source, in Kotlin

class GeoTiffSource(private val path: String) : TileDataSource(0, 18) {

override fun loadTile(tile: MapTile): TileData? {
// Your own decode. The SDK asked for one tile of its grid; hand back an image.
val png = NativeGdal.renderTile(path, tile.zoom, tile.x, tile.y) ?: return null
return TileData(BinaryData(png))
}
}

loadTile runs on the SDK's tile threads, not the UI thread, and several at once — it has to be thread-safe. Each call crosses JNI, which is irrelevant next to a decode but would not be next to a memory lookup.

Then register it and use it like any other source:

val source = GeoTiffSource("/sdcard/dem.tif")
MassifInterop.adopt("source", "dem", source)

map.addLayer("dem", Spec.of("raster").set("source", map.source("dem")))

A vector source

VectorDataSource is the same shape — override loadElements(cullState) and return a VectorData of VectorElements you built. Because the elements are yours, so is the styling: pick a MarkerStyle or LineStyle per feature in Kotlin, where the rules are easier to read than in any expression language.

class ShapefileSource(projection: Projection) : VectorDataSource(projection) {
override fun loadElements(cullState: CullState): VectorData {
val elements = VectorElementVector()
for (feature in readFeatures(cullState.viewState)) {
elements.add(Marker(feature.pos, if (feature.big) bigStyle else smallStyle))
}
return VectorData(elements)
}
}

A tile source in C or C++

An extension whose real work is native — GDAL, a proprietary decoder — does not have to bounce through the binding language. mm_source_create_custom registers a source implemented as a plain C function pointer:

#include <MassifApiC.h>

static int load_tile(void* user, int z, int x, int y, mm_tile_sink sink, void* sink_data) {
unsigned char rgba[256 * 256 * 4];
if (!render_tile(user, z, x, y, rgba)) {
return MM_OK; /* no such tile - a hole, not an error */
}
/* Copied inside this call, so the stack buffer above is fine and there is nothing to free. */
sink(sink_data, rgba, sizeof(rgba), MM_TILE_RGBA8, 256, 256);
return MM_OK;
}

mm_tile_source source = { 0, 14, load_tile, close_dataset, dataset };
mm_handle handle;
mm_source_create_custom(mm_context_default(), "dem", &source, &handle);

The id then works exactly as an adopted one does — Spec.of("raster").set("source", "dem").

MM_TILE_RGBA8 hands over decoded pixels, which is the point: the Kotlin path above has to encode a PNG that RasterTileLayer immediately decodes again. Use MM_TILE_ENCODED when what you have really is a file, including vector tile protobuf for a "vector" layer.

Link against libmassif.so for the mm_* symbols only — they are the exported surface, and the extension's own .so is otherwise independent of the SDK.

What an extension cannot do

  • Register a spec type. The spec table is generated at build time, so {"type": "geotiff"} in JSON is not available. Construct the source and adopt it, or use mm_source_create_custom; specs then reference it by id or handle.
  • Subclass an SDK class from C++. The SDK compiles into one library with -fvisibility=hidden and an Android link that exports only Java_*, CSharp_*, SWIG* and mm_*, so another native library cannot derive from massif::TileDataSource however many headers it has. That is what mm_source_create_custom exists for — a function pointer instead of a vtable.
  • Supply vector ELEMENTS from C. The C ABI covers tile sources. A VectorDataSource producing VectorElements is a binding-language subclass, as above.
  • Avoid thread safety. Every one of these — the two overrides and the C loader — is called concurrently from tile and culling threads.

Why this rather than a build profile

An earlier attempt put GDAL in the SDK: a gdal build profile, a full+gdal published variant, a CI job cross-building GDAL for four ABIs, and a plan to vendor GDAL and PROJ into libs-external. It worked on paper and made every SDK build carry a GIS dependency's problems — proj.db's nine megabytes, five iOS slices, a driver set nobody agreed on.

The extension does the same job with no SDK change at all. That is the pattern for anything with a heavy native dependency: if the SDK does not need it to draw a map, it belongs in an extension.