Native plugins › 03 Data in and out
Read arguments correctly and return values safely. Values are untagged, so types come from metadata, never from guesswork.
typedef struct NativeCtx {
VM* vm; /* allocation, exceptions, class lookup */
Value recv; /* this (the singleton itself for %nativesimple) */
Method* method; /* name, descriptor, arity, return type */
Value* args; /* arguments, length argc (varargs included) */
uint32_t argc;
const TypeDesc** arg_types; /* static type of each argument */
const TypeDesc** slots; /* generic slot bindings, in declaration order */
uint32_t nslots;
const TypeDesc* ret_type;
Value out; /* return value; held as a GC root during the call */
} NativeCtx;
Value is an untagged union. Always read through the accessors,
which check the static type:
int32_t a = n_as_int(n_arg(ctx, 0), n_arg_type(ctx, 0));
int64_t b = n_as_long(n_arg(ctx, 1), n_arg_type(ctx, 1));
double d = n_as_double(n_arg(ctx, 2), n_arg_type(ctx, 2));
bool f = n_as_bool(n_arg(ctx, 3), n_arg_type(ctx, 3));
ObjString* s = n_as_string(n_arg(ctx, 4), n_arg_type(ctx, 4));
Obj* o = n_as_obj(n_arg(ctx, 5), n_arg_type(ctx, 5));
n_as_int requires int; n_as_long
accepts int or long; n_as_double is
numeric-loose and accepts all six numeric types.n_arg bounds-checks; n_arg_type returns the
static type.n_is_*(n_arg_type(ctx, i)) to branch on types, never the
raw bits.ctx->out.i = 42; /* int/long/byte/short/bool */
ctx->out.d = 3.14; /* float/double */
ctx->out = vm_new_string(ctx->vm, "hi", 2); /* String */
ctx->out = n_arg(ctx, 0); /* pass a reference through */
ctx->out is a GC root for the duration of the call, so writing a
freshly allocated object into it is safe. Do not allocate a second object between
creating one and storing it into out — use roots for that (chapter
5).
vm_new_string copies bytes into a fresh immutable string.
Strings cannot be mutated. Object references arrive as Obj*; if you
need to call language methods on them, use vm_call (chapter 6).
%native com.example.Log
void debug(String tag, ...)
static void nd_debug(NativeCtx* ctx) {
for (uint32_t i = 0; i < ctx->argc; i++) {
const TypeDesc* t = ctx->arg_types[i];
if (n_is_string(t)) {
ObjString* s = n_as_string(ctx->args[i], t);
/* use s->data / s->len */
}
}
}
... may only be the last parameter of a native method; fixed
parameters are arity-checked, the rest are passed through with no implicit
conversion.ctx->arg_types[i] gives the static type of each argument,
including the variadic ones.%native com.example.EqSet<?a>
void put(?a v)
bool contains(?a v)
const TypeDesc* elem = ctx->nslots > 0 ? ctx->slots[0] : NULL;
if (elem && n_is_ref(elem)) {
/* reference elements: use content equality via vm_call */
} else {
/* primitive elements: compare numerically */
}
Slots are encoded in descriptors as L?0;,
L?1;, and their bound types are available through
ctx->slots (or n_slot_type(ctx, i)). A slot may itself
be a nested generic. The declaration and the library must agree on slot names;
binding checks that.