Native plugins › 01 How native classes work

How native classes work

Understand the split between the declaration in .loth and the implementation in C, and how the two are matched.

1. Two halves

.loth declaration ──┐
                    ├── matched by name + descriptor ──▶ binding
foo.so implementation ─┘

A native class is declared in Lopolith like any other type, but with no bodies:

%native com.example.counter.Counter
    pub int scale
    void ___init(int start)
    void add(int d)
    int value()

The C side provides one function per method, all with the same signature void fn(NativeCtx* ctx), and registers them with descriptors that must match the declaration exactly.

2. Descriptors

Every method is identified by a name plus a descriptor string:

TypeDescriptor
bool byte short int long float double voidZ B S I J F D V
StringL_LANG.String;
class com.example.FooLcom.example.Foo;
arrays[I, [L_LANG.String;, [[I
method(II)Lcom.example.Foo;, ()V
generic slotL?0;, L?1;
class token &T&Lcom.example.T;

Class names inside descriptors are dotted fully-qualified names. There are no generics or overloading beyond what the declaration says: a method is matched by its exact name and descriptor.

3. Registration: V2 and V1

A library exports one function. The modern (V2) form registers several classes, each with kind, fields, interfaces and slots:

typedef struct NativeClassMeta {
    const char* class_name;
    int kind;                        /* 0 = newable (%native); 1 = singleton (%nativesimple) */
    const char* const* implements;   /* dotted FQNs, NULL-terminated */
    const char* const* slots;        /* slot names, NULL-terminated */
    const NativeFieldReg* fields;    int nfields;
    const NativeMethodReg* methods;  int nmethods;
    void (*finalize)(VM* vm, ObjNative* obj);
    void (*gc_scan)(VM* vm, ObjNative* obj);
} NativeClassMeta;

const NativeClassMeta** lopolith_get_native_classes(uint32_t* out_n);

The older V1 form registers a single class with lopolith_get_native; the loader prefers V2 and falls back to V1 for existing libraries. New plugins should use V2.

4. Binding and validation

Binding happens once, when the host calls loth_engine_bind_native (or dynamically via vm_bind_native_file). The loader:

  1. Finds the library: by class name in the classpath, then the standard library directory, then the working directory; if the file name does not match, it scans the library directories for a registration.
  2. Opens it and reads its registry.
  3. Validates the metadata against the .loth declaration: class kind, every field (name and descriptor), every method (name and descriptor), interfaces and slot names. Both directions are checked — the declaration may not promise a method the library does not provide.
  4. Refuses mismatches instead of half-binding. A rejected class is reported and left unbound; calling it later panics with "unimplemented".

5. Kinds

DeclarationKindInstance
%native0created with new
%nativesimple1one singleton, initialized on first use

Both can declare fields, methods, interfaces and slots. A singleton's constructor takes no parameters.

6. What the C side sees

Every method receives a NativeCtx: the receiver, the arguments with their static types, the VM, and the output slot. Chapter 3 covers it in detail; the rest of this track builds the example plugin piece by piece.