Skip to content

Native Objects and Values

s edited this page Aug 21, 2026 · 4 revisions

Native Objects and Values

V3 has two deliberately different ways to move structured data through the JavaScript boundary:

  • a native object is a persistent reference to the original C++, Kotlin, or Java instance; and
  • a value object is a declared, validated snapshot copied across the bridge.

The public JavaScript and TypeScript model is the same regardless of which implementation language owns the declaration.

Native reference objects

Declare reference semantics with SupernotePluginObject. Marking the type does not automatically expose its public members or construction. Mark every JavaScript-visible method or field with SupernotePluginExport, and use SupernoteConstructor only when JavaScript should construct the type.

// @SupernotePluginObject
class Stroke {
 public:
  // @SupernoteConstructor
  explicit Stroke(std::string id);

  // @SupernotePluginExport
  bool intersects(const Stroke &other) const;
};

The equivalent Kotlin or Java declaration produces the same conceptual API:

const first = feature.Stroke.create('first');
const second = feature.Stroke.create('second');
first.intersects(second);

An object does not need a JavaScript constructor. An exported function or method may return a returned-only object, which can still be stored, passed to compatible APIs, used through marked methods and fields, and retained by accepted asynchronous work.

JavaScript holds the original instance rather than a copy. Re-exposing the same live instance in one runtime and active feature generation returns the same wrapper, so a === b. C++ wrappers own std::shared_ptr<T> state; JVM wrappers hold managed JNI references and compare identity with IsSameObject.

Current V3 object routes are same-family:

  • C++ objects may be passed to C++ declarations; and
  • Kotlin and Java objects may be passed within the JVM family.

Passing a C++ object to a JVM-only route, or a JVM object to a C++-only route, is rejected safely. C++/JVM object proxies are deferred. The public nominal type model is language-neutral so future proxies do not require another JavaScript API redesign.

Declared copied values

Declare copied structural data with SupernotePluginValue. A JavaScript object such as {x, y} maps only to a supported declared C++ aggregate, Kotlin data class, Java record, or supported final Java value class.

// @SupernotePluginValue
struct Point {
  // @SupernotePluginExport
  double x;

  // @SupernotePluginExport
  double y;
};
const point: feature.Point = {x: 12, y: 34};

Every declared field is required and strictly validated. Extra JavaScript properties are ignored without being read or copied. Values have no persistent native identity, and results are fresh JavaScript containers.

Declared values, enums, typed arrays, and nullable compositions can cross a generated internal C++/JVM route when both language projections describe the same logical schema. A native-object leaf still follows the same-family rule.

Supported type system

Meaning JavaScript / TypeScript C++ Kotlin Java
No result void void Unit void
Boolean boolean bool Boolean boolean
32-bit integer number std::int32_t Int int
64-bit integer bigint std::int64_t Long long
32-bit float number float Float float
64-bit float number double Double double
String string std::string String String
Bytes Uint8Array std::vector<std::byte> ByteArray byte[]
String enum string-literal union marked enum class marked enum class marked enum
Value object typed plain object marked aggregate marked data class marked record/final class
Native object generated nominal interface std::shared_ptr<T>-backed original object original object
Typed array T[] std::vector<T> List<T> List<T>
Nullable T or null std::optional<T> T? JSpecify @Nullable T

Arrays are dense and homogeneous. They may contain any supported non-void type, including native-object references that satisfy the route family. Uint8Array remains the distinct bytes type rather than becoming number[]. A nullable field is still required; it may contain null, but it may not be missing or undefined. In TypeScript, a nullable position is spelled T | null.

V3 deliberately rejects arbitrary JavaScript/JSON trees, any, callbacks, functions, Map, Set, Date, symbols, mixed or sparse arrays, tuples, general unions, numeric native handles, raw pointers, unsigned integers, and recursive by-value schemas.

Safe inspection and routing

Generated companions and callables expose non-invoking checks:

feature.Stroke.is(value);
feature.Point.is(value);
feature.intersects.accepts(first, second);
feature.intersects.checkArguments(first, second);

.is(value) is exact and nominal for native objects, structural for declared values, and exact-name based for string enums. .accepts(...args) returns a boolean without calling the C++, Kotlin, or Java implementation. .checkArguments(...args) returns structured validation information using the same error class, path, reason, expected type, and actual type as the real call.

This makes fallback routing recoverable:

if (cppOnlyApi.accepts(value)) return cppOnlyApi(value);
if (jvmOnlyApi.accepts(value)) return jvmOnlyApi(value);
throw new TypeError('Expected a supported renderer object');

nativeObjectInfo(value) may report the public semantic type and a diagnostic originFamily of cpp or jvm. It never exposes a pointer, JNI reference, or registry handle. Use .accepts for routing; the origin family is diagnostic information, not a permanent compatibility promise.

Lifetime, async work, and reload

Accepted asynchronous work retains its receiver and every native-object argument until the implementation can no longer access them. Copied values, arrays, strings, and bytes are owned snapshots before work leaves the JavaScript thread. Garbage collection does not cancel accepted work.

Lifetime management does not make user objects thread-safe. Synchronous and asynchronous calls may overlap, so mutable C++, Kotlin, and Java state still needs an appropriate mutex, serial executor, actor, or immutable design.

Bundle replacement invalidates the old runtime generation. Late work cannot call into the replacement JavaScript runtime, and old wrappers are never rebound to new native capabilities. Generated worker and cleanup services start only when needed and shut down when the final session for that runtime generation is invalidated.

PluginHost retains at most 32 generated native generations in one process. The 33rd changed native generation fails closed with a restart instruction. A JavaScript-only npm run send changes the bundle but does not rebuild or reinstall native code; use the plugin template's normal run and diagnostic scripts to distinguish sending, launching, reinstalling, and recovery.

Clone this wiki locally