Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
294634f
Add the browser module contract and the Kotlin/Wasm binding foundation
sargunv-bot Aug 4, 2026
16cb8ae
Add UTF-8 heap helpers to the browser binding
sargunv-bot Aug 4, 2026
b659fc4
Scaffold the remaining wasmJs actuals so the source set compiles
sargunv-bot Aug 4, 2026
b1af2c7
Implement the browser binding's remaining actuals and run them
sargunv-bot Aug 4, 2026
cd249a3
Cover the browser binding against the specification, and fix what tha…
sargunv-bot Aug 4, 2026
327aafa
Present browser frames to a page canvas without copying them
sargunv-bot Aug 4, 2026
0637658
Make the browser binding usable by a host, and prove CI would notice
sargunv-bot Aug 4, 2026
3c7cbcd
Make the browser render context safe to own, and say what it costs
sargunv-bot Aug 5, 2026
457b47b
Serve custom geometry sources from the page without blocking a worker
sargunv-bot Aug 5, 2026
0fab92b
Make teardown enforceable and stop a descriptor outliving its context
sargunv-bot Aug 5, 2026
7eda5c0
Let a browser host answer a tile request the way every other target does
sargunv-bot Aug 5, 2026
8f34fd3
Account for every handle at shutdown, and let go of the module
sargunv-bot Aug 5, 2026
342de2c
Retire a browser callback the way the specification says, and stop re…
sargunv-bot Aug 5, 2026
e18939c
Prove the failure paths the suite had only asserted were unreachable
sargunv-bot Aug 5, 2026
58e6820
Report an allocation failure instead of aborting, and close the last …
sargunv-bot Aug 5, 2026
c36d86a
Keep the drain alive when the heap will not serve it, and prove the t…
sargunv-bot Aug 5, 2026
c230718
Scope a proxied diagnostic to the read it belongs to, and let an idle…
sargunv-bot Aug 5, 2026
3cd63d4
Regenerate the CI workflow after re-aligning onto main
sargunv-bot Aug 5, 2026
3a581fe
Delete the machinery that existed because the page could not block
sargunv-bot Aug 5, 2026
a06ef92
Fix log consumption at registration, the way the other bindings do
sargunv-bot Aug 5, 2026
df0692d
Reach the module the way every other binding reaches its library
sargunv-bot Aug 5, 2026
ce96112
Wire the browser module build to the shim it now carries
sargunv-bot Aug 6, 2026
9080791
Give the render thread a canvas the page can see
sargunv-bot Aug 6, 2026
71b4d89
Point the comments at code that exists
sargunv-bot Aug 6, 2026
f42ad1c
Give the shim's entry points a header to be declared in
sargunv-bot Aug 6, 2026
3d5ce5f
Let the generator see the shim header
sargunv-bot Aug 6, 2026
29e9d35
Name the page canvas the way both lookups spell it
sargunv-bot Aug 6, 2026
c20e74c
Say where a host puts its own program
sargunv-bot Aug 6, 2026
4feb91d
Let a host end the program it started
sargunv-bot Aug 6, 2026
fc6c531
Call the module directly, and take callbacks through the C API's adap…
sargunv-bot Aug 6, 2026
35f960a
Name a parameter something a module can bind
sargunv-bot Aug 6, 2026
4777771
Keep the submodule where main put it
sargunv-bot Aug 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .github/workflows/ci.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions .github/workflows/snapshots.yml
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,15 @@ jobs:
run-id: ${{ needs.plan.outputs.run_id }}
pattern: native-package-linux-x64-*-${{ env.SNAPSHOT_SHA }}
path: build/packages/native/maven-input
# The wasmJs publication carries the prelinked browser module, which this
# partition takes from the Emscripten package rather than linking itself.
- if: ${{ matrix.partition == 'linux' }}
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
github-token: ${{ github.token }}
run-id: ${{ needs.plan.outputs.run_id }}
name: native-package-emscripten-wasm32-webgl-${{ env.SNAPSHOT_SHA }}
path: build/packages/native/maven-input
- run: mise run //:kotlin:publish-snapshot stage ${{ matrix.partition }}
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
Expand Down Expand Up @@ -597,6 +606,15 @@ jobs:
run-id: ${{ needs.plan.outputs.run_id }}
pattern: native-package-linux-x64-*-${{ env.SNAPSHOT_SHA }}
path: build/packages/native/maven-input
# The wasmJs publication carries the prelinked browser module, which this
# partition takes from the Emscripten package rather than linking itself.
- if: ${{ matrix.partition == 'linux' }}
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
github-token: ${{ github.token }}
run-id: ${{ needs.plan.outputs.run_id }}
name: native-package-emscripten-wasm32-webgl-${{ env.SNAPSHOT_SHA }}
path: build/packages/native/maven-input
- run: mise run //:kotlin:publish-snapshot publish-leaves ${{ matrix.partition }}

publish-kotlin-roots:
Expand Down
13 changes: 13 additions & 0 deletions .mise/tasks/kotlin/publish-snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ prepare_native_inputs() {
android-arm64-vulkan
android-x64-egl
android-x64-vulkan
emscripten-wasm32-webgl
linux-x64-egl
linux-x64-vulkan
)
Expand Down Expand Up @@ -152,6 +153,18 @@ publish_linux_leaves() {
LinuxX64 -- \
"$@" \
-Pmaplibre.runtime.vulkan.linuxX64.installDir="$install_root/linux-x64-vulkan"

# A browser host fetches the prelinked module rather than loading a library, so
# the wasmJs publication carries that module as a classified archive instead of
# depending on a runtime publication. The module comes out of the browser
# package the same way every other target's library does, which is why this
# publishes from the partition that has the Emscripten artifact.
publish_tasks \
:bindings:kotlin \
"$repository" \
WasmJs -- \
"$@" \
-Pmaplibre.browser.moduleDir="$install_root/emscripten-wasm32-webgl/lib/browser"
}

publish_apple_leaves() {
Expand Down
32 changes: 32 additions & 0 deletions .mise/tasks/kotlin/verify-staging
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ MODULES = {
"maplibre-native-ffi-runtime-vulkan-android",
"maplibre-native-ffi-runtime-vulkan-jvm",
"maplibre-native-ffi-runtime-vulkan-linuxx64",
"maplibre-native-ffi-wasm-js",
}

# Modules with public Kotlin API. Their javadoc jars must carry the Dokka site,
Expand All @@ -41,6 +42,7 @@ API_BEARING_MODULES = {
"maplibre-native-ffi-jvm",
"maplibre-native-ffi-linuxx64",
"maplibre-native-ffi-macosarm64",
"maplibre-native-ffi-wasm-js",
}

ROOT_TARGET_MODULES = {
Expand All @@ -51,6 +53,7 @@ ROOT_TARGET_MODULES = {
"jvm": "maplibre-native-ffi-jvm",
"linuxX64": "maplibre-native-ffi-linuxx64",
"macosArm64": "maplibre-native-ffi-macosarm64",
"wasmJs": "maplibre-native-ffi-wasm-js",
},
"maplibre-native-ffi-runtime-opengl": {
"android": "maplibre-native-ffi-runtime-opengl-android",
Expand Down Expand Up @@ -382,6 +385,34 @@ def verify_native(root: pathlib.Path) -> None:
raise SystemExit(f"{interop} is missing {expected}")


BROWSER_MODULE_FILES = {
"maplibre_native_c.mjs",
"maplibre_native_c.wasm",
}


def verify_browser(root: pathlib.Path) -> None:
"""A browser host fetches the module rather than linking it, so the wasmJs
publication carries it as a classified archive instead of taking it from a
runtime module. An archive missing either file publishes a binding no page
can load."""
archive = one(
module_files(root, "maplibre-native-ffi-wasm-js", "-browser-module.zip"),
"browser module archive",
)
with zipfile.ZipFile(archive) as zip_file:
names = set(zip_file.namelist())
missing = sorted(BROWSER_MODULE_FILES - names)
if missing:
raise SystemExit(f"{archive} is missing {missing}")
# The module is statically linked, so it redistributes MapLibre Native and every vendored
# dependency inside the wasm. Shipping it without their notices is a licensing defect that
# nothing about the archive makes visible, which is why it is checked before publication
# rather than left to a reader.
if not any(name.startswith("licenses/") and not name.endswith("/") for name in names):
raise SystemExit(f"{archive} carries no third-party notices under licenses/")


def main() -> int:
if len(sys.argv) != 2:
print(
Expand All @@ -395,6 +426,7 @@ def main() -> int:
verify_android_runtime_independence(root)
verify_jvm(root)
verify_native(root)
verify_browser(root)
return 0


Expand Down
5 changes: 5 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ mln_ffi_install_c_api_library(maplibre_native_c)
# they never depend on the bare target name.
add_library(maplibre_native_ffi::c ALIAS maplibre_native_c)

# The browser's distributable artifact is a linked module rather than a library,
# so it is produced here rather than by whatever consumes it.
include(mln_ffi_browser_module)
mln_ffi_add_browser_module(mln_ffi_browser_module maplibre_native_c)

if(BUILD_TESTING AND MLN_FFI_ARTIFACT_NAME)
include(cmake/mln_ffi_tests.cmake)
mln_ffi_add_c_api_test()
Expand Down
33 changes: 33 additions & 0 deletions bindings/kotlin/browser-test/maplibre-native-kotlin.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// The Kotlin distribution the module boots, for the browser test suite.
//
// The module imports `./maplibre-native-kotlin.mjs` on the pthread
// -sPROXY_TO_PTHREAD gives main() and calls `mlnKotlinMain()`. An application
// serves its own distribution under that name and exports the entry point from
// Kotlin. A test binary has no entry point to export: the compiler emits the
// suite as `startUnitTests`, which only JavaScript can call. So this stands in
// for the application, and everything below it is the same Kotlin the module
// would boot in production.
import * as suite from "./maplibre-native-kotlin-suite.mjs";

export function mlnKotlinMain() {
let status;
try {
suite.mlnKotlinTestBegin();
suite.startUnitTests();
status = suite.mlnKotlinTestFailures() === 0 ? 0 : 1;
} catch (error) {
console.error("maplibre: the browser suite did not finish", error);
status = 70;
}

// The status leaves this thread through the process exit, because nothing
// else crosses back to the host: the suite runs in a realm of its own, and
// main() is holding a runtime keepalive that would otherwise keep the module
// alive with nothing left to run. mln_kotlin_exit() clears that keepalive on
// the host's thread and exits there, which is where Module.onExit is raised.
try {
globalThis.Module._mln_kotlin_exit(status);
} catch {
// Emscripten unwinds the calling thread by throwing out of exit().
}
}
Loading
Loading