-
Notifications
You must be signed in to change notification settings - Fork 0
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.
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.
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.
| 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.
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.
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.