Skip to content

Latest commit

 

History

History
142 lines (98 loc) · 3.75 KB

File metadata and controls

142 lines (98 loc) · 3.75 KB

Callback Design

Goal

Callbacks must work for real. Python code should import Java host contracts as Python modules and call them naturally.

Annotation-driven via context (recommended)

Use context on @ScriptInterface to define both the guest contract and the host callbacks:

class HostApi {
    @HostFunction
    public void log(String message) { System.out.println(message); }

    @HostFunction
    public User findUser(String id) { return userService.find(id); }
}

@ScriptInterface(module = "my_script", context = HostApi.class)
public interface MyScript {
    Result run(String input);
}

The processor generates MyScript_Builtins from the context class:

engine.expose(MyScript_Builtins.toHostModule(new HostApi()));

Standalone @Builtins

For callbacks without a corresponding guest contract, use @Builtins directly:

@Builtins("host")
class HostApi {
    @HostFunction
    public void log(String message) { System.out.println(message); }
}

engine.expose(HostApi_Builtins.toHostModule(new HostApi()));

Low-level API

engine.expose("host",
    new HostFunction("log", List.of(String.class), Void.class,
        args -> { System.out.println(args.get(0)); return null; }));

Python:

from host import log, findUser

def run(id):
    user = findUser(id)
    log(user["name"])
    return user

Boundary

The callback path is purely FFM, no native helper:

Python call
  -> PyCFunction backed by FFM upcall stub
  -> Java callback dispatcher
  -> Java host contract implementation
  -> result converted back to Python

Pure FFM approach

Host modules are created entirely from Java using FFM struct layouts and upcall stubs. No compiled C helper is needed.

The steps are:

  1. Build PyMethodDef array in off-heap memory using MemorySegment, one entry per host contract method. Each entry's ml_meth field points to an FFM upcall stub.
  2. Build PyModuleDef struct pointing to the method table.
  3. Register a module init function via PyImport_AppendInittab before Py_Initialize. The init function is itself an FFM upcall stub that calls PyModule_Create.
  4. When Python imports the host module, CPython calls the init function, which creates the module with the method table.
  5. When Python calls a host function, CPython invokes the PyCFunction pointer, which is the FFM upcall stub. The stub enters the Java dispatcher.

Dispatcher shape

The FFM upcall stub receives CPython's standard PyCFunction signature:

PyObject* callback(PyObject* self, PyObject* args)

The Java dispatcher:

  • identifies the host contract and method from the upcall context;
  • converts Python arguments using primitive/JSON rules;
  • invokes the Java method;
  • converts the Java return value back to a Python object;
  • maps Java exceptions to Python exceptions via PyErr_SetString.

Java dispatcher responsibilities

  • find the exposed host contract by module name;
  • find the Java method by Python function name;
  • convert Python arguments using primitive/JSON rules;
  • invoke Java method;
  • convert Java return value back to Python;
  • map Java exceptions to Python exceptions.

Exception mapping

Python -> Java:

Python exception -> PythonException with type, message, traceback

Java callback -> Python:

Java exception -> PyErr_SetString -> Python RuntimeError

GIL policy

All Java entry into Python must acquire the GIL.

Python calls into Java while the GIL is held. V1 should keep callbacks synchronous.

Reentrancy

V1 supports the common pattern:

Java -> Python contract method -> Java host callback -> Python returns -> Java returns

Deeper recursive reentry should be tested and either supported with guardrails or explicitly rejected with a clear error.