ajni

    Android JNI runtime primitives and MoonBit lifecycle bridge.

    moonbit
    android
    jni
    ndk
    Download zip
    Version
    0.2.4
    License
    Apache-2.0
    Last updated
    11 hours ago
    Downloads
    515

    #ajni

    ajni provides MoonBit-facing JVM JNI primitives and an Android runtime bridge. The root package builds checked JVM class, type, method, and native-method declarations; the Android package supplies a stable Kotlin host, lifecycle and UI-thread callbacks, safe Java UTF-16/MoonBit UTF-8 conversion, and an optional Android WebView feature.

    The generic, Android, and WebView packages are separate. Applications that only need JVM JNI declarations import Nanaloveyuki/ajni; Android hosts add Nanaloveyuki/ajni/android; applications that embed a browser additionally import Nanaloveyuki/ajni/webview and link its native stub.

    #Install

    moon add Nanaloveyuki/ajni

    For the optional WebView feature, import its package from a MoonBit package:

    import {
    "Nanaloveyuki/ajni/webview",
    }

    #Generic JNI Declarations

    The root package has no Android dependency. It validates internal class names, prevents void parameters, enforces the JVM's 255 array-dimension and parameter-slot limits, and keeps raw JNI pointers out of MoonBit:

    let string = try! @ajni.JniClass::parse("java/lang/String")
    let signature = try! @ajni.JniMethod::new(
    [@ajni.JniType::object(string)],
    return_type=@ajni.JniType::boolean(),
    )
    let registration = try! @ajni.NativeMethod::new("nativeAcceptsString", signature)
    println(registration.descriptor()) // (Ljava/lang/String;)Z

    Platform-specific code can consume these values when registering JNI methods. Raw JNIEnv*, jobject, global-reference deletion, and Java attachment remain C-owned because their lifetimes cannot be safely encoded as plain MoonBit values.

    #Android Runtime

    Import Nanaloveyuki/ajni/android to install an event handler before the Kotlin host forwards Activity or Surface events. The handler returns a token that can be removed during shutdown.

    import {
    "Nanaloveyuki/ajni/android",
    }

    let subscription = @android.install_event_handler(event => match event {
    @android.AndroidEvent::Lifecycle(@android.Lifecycle::Resumed) => println("resumed")
    @android.AndroidEvent::UiTask => println("Android main Looper callback")
    _ => ()
    })

    // Remove the observer before application shutdown.
    @android.remove_event_handler(subscription)

    @android.post_to_ui() schedules an asynchronous callback on Android's main Looper. @android.start_worker() demonstrates a native-owned thread attaching to ART, posting back to the UI thread, then detaching.

    #Android Host

    The Android application links the generated MoonBit native artifact and uses the reusable Kotlin host from the android:host Gradle module. Initialize it once, then forward the Activity lifecycle and attach a caller-owned container on Android's main thread:

    import dev.nanaloveyuki.ajni.host.NativeBridge NativeBridge.initialize(applicationContext) NativeBridge.attachWebViewContainer(container) // Forward every Activity lifecycle state on the main thread. NativeBridge.lifecycle(state) // During Activity teardown: NativeBridge.detachWebViewContainer(container) NativeBridge.shutdown()

    NativeBridge.LIFECYCLE_RESUMED and NativeBridge.LIFECYCLE_PAUSED also resume and pause attached WebViews. Attaching a replacement container destroys views in the previous container, so each Activity or host recreation starts from an explicit new create call.

    Use android/app/src/main/cpp/CMakeLists.txt as the integration template. It generates the MoonBit Android host, compiles the MoonBit runtime, and links libandroid and liblog. The configured minimum Android API is 24; supported demo ABIs are arm64-v8a and x86_64.

    #WebView Feature

    The WebView host must be attached before @webview.create. Commands are queued on Android's main Looper when called from another thread. Each view has a caller-provided Int64 handle.

    import {
    "Nanaloveyuki/ajni/webview",
    }

    let subscription = @webview.install_event_handler(event => match event.kind {
    @webview.EventKind::Created(_) => println("ready")
    @webview.EventKind::ScriptResult(request_id, json) =>
    println("\{request_id}: \{json}")
    @webview.EventKind::PageMessage(body, origin, _is_main_frame) =>
    println("\{origin}: \{body}")
    @webview.EventKind::AssetRequest(request) => println(request.path)
    @webview.EventKind::OperationFailed(operation_id, message) =>
    println("\{operation_id}: \{message}")
    _ => ()
    })

    try! @webview.create(
    1L,
    "https://app.example.test",
    @webview.InitialContent::Url("https://app.example.test/assets/index.html"),
    document_start_scripts=["globalThis.appReady = true"],
    operation_id="create-browser",
    )
    try! @webview.eval(1L, "document.title", "title-request")
    try! @webview.destroy(1L, operation_id="destroy-browser")
    @webview.remove_event_handler(subscription)

    Importing only the core package does not compile src/webview/ajni_webview_bridge.c. An Android build that imports ajni/webview must also include that stub, define AJNI_FEATURE_WEBVIEW=1, and export ajni_dispatch_webview_event; the bundled CMake template demonstrates all three requirements.

    The host enables JavaScript for eval, disables file and content access, disables mixed content and multiple windows, enables Safe Browsing on Android 8+, and does not expose addJavascriptInterface. Web messages are delivered only through AndroidX WebKit's trusted-origin listener. Embedded resources use the same origin's /assets/ path and emit AssetRequest; complete them with respond_asset before the configured timeout.

    #Build And Verify

    Host-side checks:

    moon fmt --check moon check --target native moon test --target native -v

    For an Android build, install Android SDK platform tools, NDK 29.0.14206865, CMake, Java 17, Gradle, and MoonBit. The bundled demo can run its emulator tests with:

    $env:ANDROID_SDK_ROOT = "C:\path\to\Android\Sdk" $env:ANDROID_NDK_HOME = "C:\path\to\android-ndk-r29" $env:ANDROID_SERIAL = "emulator-5554" gradle -p android :app:connectedDebugAndroidTest

    The GitHub workflow builds the Android demo for both configured ABIs. Device and emulator validation remain appropriate for changes that alter Android UI or WebView behavior.

    #Contributing

    See CONTRIBUTING.md for feature boundaries, validation, and pull request requirements.

    #ajni JNI

    Nanaloveyuki/ajni provides checked JVM JNI declarations without an Android dependency. It intentionally does not expose raw JNIEnv*, jobject, or function pointers because their lifetime rules cannot be represented safely by plain MoonBit values.

    #JNI Descriptors

    ///|
    test {
    let string = try! @ajni.JniClass::parse("java/lang/String")
    let signature = try! @ajni.JniMethod::new(
    [@ajni.JniType::object(string)],
    return_type=@ajni.JniType::boolean(),
    )
    let registration = try! @ajni.NativeMethod::new(
    "nativeAcceptsString", signature,
    )
    assert_eq(registration.descriptor(), "(Ljava/lang/String;)Z")
    }

    JniType cannot represent void; arrays and method parameters are bounded by JVM limits. Import Nanaloveyuki/ajni/android for Android lifecycle, Surface, worker, and main-Looper callbacks. Browser support remains in the optional Nanaloveyuki/ajni/webview package.

    JniError

    pub(all) suberror JniError {
    NotInitialized
    RuntimeDestroyed
    NativeFailure(String)
    JavaException(String)
    InvalidClassName(String)
    InvalidNativeMethodName(String)
    DescriptorTooLong
    TooManyParameterSlots
    TooManyArrayDimensions
    } derive(
    Debug
    )

    JNI failures represented as values at the MoonBit boundary.

    JniClass

    pub struct JniClass {
    // private fields
    } derive(Eq,
    Debug
    )

    An internal JVM binary class name, for example java/lang/String.

    JniClass::descriptor

    fn JniClass::descriptor(self : JniClass) -> String

    JniClass::internal_name

    fn JniClass::internal_name(self : JniClass) -> String

    JniClass::parse

    fn JniClass::parse(internal_name : String) -> JniClass raise JniError

    JniMethod

    pub struct JniMethod {
    // private fields
    } derive(Eq,
    Debug
    )

    A validated JVM method descriptor. The default return type is void.

    JniMethod::descriptor

    fn JniMethod::descriptor(self : JniMethod) -> String

    JniMethod::new

    fn JniMethod::new(parameters : ArrayView[JniType], return_type? : JniType) -> JniMethod raise JniError

    JniType

    pub struct JniType {
    // private fields
    } derive(Eq,
    Debug
    )

    A non-void JNI value type. Its parameter-slot width is retained so method descriptors cannot undercount long and double.

    JniType::array

    fn JniType::array(element : JniType) -> JniType raise JniError

    JniType::boolean

    fn JniType::boolean() -> JniType

    JniType::byte

    fn JniType::byte() -> JniType

    JniType::char

    fn JniType::char() -> JniType

    JniType::descriptor

    fn JniType::descriptor(self : JniType) -> String

    JniType::double

    fn JniType::double() -> JniType

    JniType::float

    fn JniType::float() -> JniType

    JniType::int

    fn JniType::int() -> JniType

    JniType::long

    fn JniType::long() -> JniType

    JniType::object

    fn JniType::object(class : JniClass) -> JniType

    JniType::short

    fn JniType::short() -> JniType

    NativeMethod

    pub struct NativeMethod {
    // private fields
    } derive(Eq,
    Debug
    )

    A native-method registration declaration without a raw function pointer.

    NativeMethod::descriptor

    fn NativeMethod::descriptor(self : NativeMethod) -> String

    NativeMethod::name

    fn NativeMethod::name(self : NativeMethod) -> String

    NativeMethod::new

    fn NativeMethod::new(name : String, signature : JniMethod) -> NativeMethod raise JniError

    decode_utf8

    fn decode_utf8(bytes : BytesView) -> String

    Decodes UTF-8 received from native code. Invalid sequences, including a replacement emitted for isolated Java UTF-16 surrogates, are lossily normalised instead of allowing invalid text into the public API.

    encode_utf8

    fn encode_utf8(text : String) -> Bytes

    Powered by MoonBit

    Site sourceReport issuePackagesBuild queueSkillsStatistics

    © 2026 mooncakes.io