Native plugins › 03 Data in and out

Data in and out

Read arguments correctly and return values safely. Values are untagged, so types come from metadata, never from guesswork.

1. The call context

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;

2. Reading arguments

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));

3. Returning values

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).

4. Strings and objects

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).

5. Variadic methods

%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 */
        }
    }
}

6. Generic slots

%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.