Skip to content

JSI Modules

s edited this page Aug 8, 2026 · 13 revisions

JSI Modules

The CLI calls this type JSI Module.

Use it only when JavaScript genuinely needs to call C++ directly through a synchronous low-overhead API.

Do not choose JSI only because you prefer C or C++. When the result can be returned asynchronously, especially when the feature needs Android APIs or substantial Kotlin or Java integration, a Native JNI Module is normally easier and safer.

When JSI is appropriate

JSI is especially useful when:

  • JavaScript genuinely needs the result synchronously.
  • The operation is short and has predictable worst-case time.
  • The operation may be called frequently enough that the normal Native Module path materially affects the feature.
  • The target PluginHost and device policy have been tested and allow the generated library to execute.

Possible examples include short mathematical or geometric operations and small direct queries of native state.

JSI can also read files, use the network, wait for other work, or perform large computations. None of that is unsupported just because the API is synchronous.

The tradeoff is that JavaScript cannot continue until the exported function returns. If the call takes a long time, the plugin may appear frozen for that entire time. That may be acceptable when the blocking is deliberate; measure it on the target Supernote and decide based on your feature.

Generate the module

supernote-module doctor --type jsi
supernote-module add fast-math-jsi --type jsi --yes

The examples below use:

Package name:     fast-math-jsi
JavaScript name: FastMath

JSI generation requires CMake 3.22.1 or newer and an Android NDK whose Clang compiler supports the generated C23 and C++23 code for arm64-v8a.

Doctor checks the local toolchain. It cannot prove runtime support on a target Supernote.

C++ examples and C support

This page uses C++ examples for clarity, but the same workflow and build setup apply to both C and C++ code.

Exported @SupernoteExport definitions are C++ because the generated JSI boundary is C++23. C code is fully supported as implementation or helper code: put it in .c files and call it from a small exported C++ wrapper through a normal extern "C" header.

What the generator handles

The generator creates:

  • C++ HostFunctions and native-object HostObject wrappers.
  • Conversion between supported JavaScript and C++ values.
  • Native installation into the JavaScript runtime.
  • Kotlin loading code.
  • CMake and Gradle configuration.
  • React Native registration.
  • The JavaScript wrapper.
  • TypeScript declarations.

You can export two kinds of API: free C++ functions and persistent C++ objects. You write ordinary marked C++ functions and classes rather than HostFunctions, HostObjects, and runtime installation code.

Where to write your code

Write C and C++ files below:

local_modules/fast-math-jsi/android/src/main/cpp/

Update preserves this complete directory, including files you add and starter files you intentionally delete.

Generated HostFunctions, loaders, registration, CMake, Gradle, package, metadata, and declaration files may be replaced by Update.

Export a C++ function

Place // @SupernoteExport immediately before an ordinary top-level C++ function definition:

// @SupernoteExport
double add(double left, double right) {
    return left + right;
}

Call it directly:

import FastMath from 'fast-math-jsi';

const total = FastMath.add(20, 22);

Do not use await. The C++ function returns before JavaScript continues.

Errors are synchronous as well.

Export a native C++ object

Define the object in a header below android/src/main/cpp/ and place the marker immediately before the complete class or struct definition:

// Counter.hpp
#pragma once

// @SupernoteExportObject
class Counter {
public:
    Counter(double initial);

    double value() const noexcept;
    void increment(double amount);

private:
    double value_;
};

Keep the implementation in an ordinary .cpp file. The generated binding includes the header; it does not include the .cpp file.

// Counter.cpp
#include "Counter.hpp"

Counter::Counter(double initial) : value_(initial) {}

double Counter::value() const noexcept {
    return value_;
}

void Counter::increment(double amount) {
    value_ += amount;
}

Create and retain the native instance through the generated factory:

import FastMath from 'fast-math-jsi';

const counter = FastMath.Counter.create(10);
counter.increment(5);
console.log(counter.value());

const otherCounter = FastMath.Counter.create(100);
otherCounter.increment(1);
console.log(otherCounter.value());

Each call to create constructs an independent C++ instance. The JavaScript object retains that instance between method calls. Its lifetime is managed automatically by JSI garbage collection and shared native ownership.

The generator creates the jsi::HostObject wrapper and owns the binding machinery. Your class stays ordinary C++: it does not inherit from a generated type, and you do not manage integer handles, a handle registry, or an explicit destroy function.

Rename the JavaScript object without changing its C++ name when needed:

// @SupernoteExportObject(name = "Document")
class NativeDocument {
public:
    NativeDocument(std::string path);
    double pageCount() const;
};

This exposes FastMath.Document.create(path).

Supported native-object API

Native objects currently support exactly one public constructor and public, non-static instance methods.

Position Supported C++ types
Constructor parameters bool, double, std::string
Method parameters bool, double, std::string
Method results void, bool, double, std::string

Methods may be unqualified, const, noexcept, or const noexcept. Parameter names are required. Default arguments are not supported.

Normal C++ access rules apply. Only public constructors and methods form the JavaScript API. Private and protected members remain native-only. Public data fields are not exported. Remember that members before an explicit access label are private in a class and public in a struct.

Current limitations

Native objects are JSI-only. The current boundary has these limits:

  • Exactly one callable public constructor; overloaded constructors are not supported.
  • No overloaded methods, static methods, or async object methods.
  • No inheritance, templates, callbacks, promises, or JavaScript subclassing.
  • No native-object parameters or return values and no arbitrary C++ object marshaling through parameters or results.
  • No default arguments.
  • No JavaScript new; construction uses ObjectName.create(...).

Unsupported public methods are reported during generation instead of being silently omitted.

Supported values

C++ JavaScript or TypeScript
bool boolean
double number
std::string string
void return void

Strings are treated as UTF-8. These value types apply to free functions, object constructors, and object methods.

Pointers, references, structs, arrays, native objects, and other C++ types cannot cross as function or method arguments and results. Exported object instances are different: they remain inside their generated first-class HostObject wrapper and are accessed through the generated JavaScript methods.

Rename the JavaScript function

// @SupernoteExport(name = "greet")
std::string make_greeting(std::string name) {
    return "Hello, " + name;
}
const message = FastMath.greet('Ziv');

Export names must be unique and valid JavaScript property names.

C and C++ files

Files ending in .cc, .cpp, or .cxx can contain exported functions.

Exported object definitions must be in .h, .hh, .hpp, or .hxx headers. Their ordinary implementations remain in .cc, .cpp, or .cxx files.

Files ending in .c compile as C23 and can contain helper code, but cannot contain @SupernoteExport functions. The generated JSI boundary is C++23.

Use normal extern "C" guards when C++ calls C helper functions.

Export restrictions

An exported function must be an ordinary top-level definition with explicitly named parameters.

The generated boundary does not support:

  • Overloaded functions.
  • Functions inside namespaces.
  • Function templates.
  • Pointer or reference parameters and returns.
  • Variadic or default arguments.
  • static, inline, constexpr, or extern "C" exported functions.
  • Export markers in .c files.

noexcept is supported.

These restrictions apply only to the exported boundary.

The JavaScript thread

A JSI function normally runs on the same thread that called it from JavaScript. JavaScript cannot continue until the function returns.

Measure:

  • The complete call.
  • Conversion cost.
  • The worst realistic input.
  • Performance on the target Supernote rather than only a desktop.

An expensive operation can still use JSI. If blocking JavaScript for its full duration is not acceptable, use JNI or another asynchronous design.

Direct access and performance

JSI avoids the Promise-based Native Module path for supported values.

It is most valuable for many short calls that truly need to be synchronous. It is not automatically faster for an entire feature.

A single batched Native or JNI call can be simpler and faster than thousands of tiny JSI calls when the actual work dominates the bridge overhead.

Choose based on data flow:

  • JSI for frequent, short, synchronous operations.
  • JNI for C or C++ work that can be requested asynchronously.
  • Native Module for Kotlin, Java, and Android APIs.

Android APIs and Kotlin or Java

JSI gives JavaScript direct access to C++, not direct access to Android APIs.

The generated package includes the Kotlin loading and Android registration required to install the JSI binding. That Kotlin is managed integration code.

When the feature needs substantial custom interaction with Android services or user-owned Kotlin and Java code, use JNI or a separate Native Module.

Manual changes to the generated loader or registration may be replaced by Update.

Other native languages

Rust, Zig, Go, and other languages with a C-compatible ABI can potentially be called through a C or C++ wrapper in the JSI module.

You are responsible for:

  • The additional compiler and Android target.
  • Static or shared library packaging.
  • Runtime requirements.
  • The wrapper.
  • CMake or Gradle changes.
  • Compatibility with PluginHost's loading environment.

From JavaScript's point of view, the export remains a generated C++ JSI function.

Runtime support on Supernote

A successful local build proves only that the package can compile.

PluginHost must also:

  1. Provide a compatible React Native and JSI runtime.
  2. Load the generated library in a compatible linker namespace.
  3. Be allowed by Android and SELinux policy to execute the library from the plugin's extracted location.

The generator cannot change firmware, PluginHost, linker namespaces, or SELinux policy.

Success on a permissive or userdebug device does not prove support on an enforcing retail device.

A Permission denied or SELinux { execute } denial occurs before your exported implementation runs.

Validate after changing exports

supernote-module validate fast-math-jsi --build

For full compiler, linker, CMake, and Gradle output:

supernote-module validate fast-math-jsi --build --verbose

Validation scans the C++ source, regenerates the binding, and compiles the library. It cannot prove that PluginHost will execute it.

Update and remove

See Managing Modules.

supernote-module update fast-math-jsi
supernote-module remove fast-math-jsi

Update preserves the complete android/src/main/cpp/ tree. Remove deletes it.