Callbacks must work for real. Python code should import Java host contracts as Python modules and call them naturally.
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()));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()));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 userThe 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
Host modules are created entirely from Java using FFM struct layouts and upcall stubs. No compiled C helper is needed.
The steps are:
- Build
PyMethodDefarray in off-heap memory usingMemorySegment, one entry per host contract method. Each entry'sml_methfield points to an FFM upcall stub. - Build
PyModuleDefstruct pointing to the method table. - Register a module init function via
PyImport_AppendInittabbeforePy_Initialize. The init function is itself an FFM upcall stub that callsPyModule_Create. - When Python imports the host module, CPython calls the init function, which creates the module with the method table.
- When Python calls a host function, CPython invokes the
PyCFunctionpointer, which is the FFM upcall stub. The stub enters the Java dispatcher.
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.
- 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.
Python -> Java:
Python exception -> PythonException with type, message, traceback
Java callback -> Python:
Java exception -> PyErr_SetString -> Python RuntimeError
All Java entry into Python must acquire the GIL.
Python calls into Java while the GIL is held. V1 should keep callbacks synchronous.
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.