-
Notifications
You must be signed in to change notification settings - Fork 0
Call-back from native to Mu #39
Comments
DetailsRelated works
More abut closuresMany languages support closures. A related work LuaJIT exposes Lua closures rather than plain stateless functions. For example: ffi.cdef[[
typedef int (__stdcall *WNDENUMPROC)(void *hwnd, intptr_t l);
int EnumWindows(WNDENUMPROC func, intptr_t l);
]]
function makeHandler(msg)
local function f(hwnd, l)
print(msg)
end
end
myHandler1 = makeHandler("I see a window!")
myHandler2 = makeHandler("Wow! A window!")
local count = 0
ffi.C.EnumWindows(myHandler1, 0) -- Implicit conversion to a callback via function pointer argument.
ffi.C.EnumWindows(myHandler2, 0) -- Implicit conversion to a callback via function pointer argument. Both LuaJIT implements this by constructing a jump table. Each exposed function pointer occupies a "slot" in the table. Each slot records the current PC (or the slot index) and then jump to the common handler where register states are inspected. By known which slot it jumps from, it is possible to distinguish between different exposed function pointers and, thus, recover the closure context. Mu differs from Lua that Mu does not directly support closures. But if the higher-level language upon Mu supports closures, Mu must provide necessary mechanism for it. |
Other implications of such "c-to-mu" interface being available: New ways to start new Mu threads: "Creating a new stack and then creating a new thread on it via the API" used to be the only way to start a Mu program. If a C thread (may be PThread) can "become" a Mu thread by "attaching to the µVM", then an easier way to start a Mu program is just calling an exposed Mu function from C (probably the main() function). Example C-based Mu loader: #include <mu.h>
int main(int argc, char** argv) {
Mu *mu = mu_new_instance();
mu_load_bundle(mu, read_file("some_bundle.uir"));
mu_attach_current_pthread(mu);
int (*mu_main)(int, char**) = mu_get_exposed_native_function(mu, "@main.native");
return mu_main(argc, argv);
} Previous approaches (via the Mu-Client API. The API still has the advantage of being language-neutral): #!/usr/bin/env python3
import sys
import mu
vm = mu.connect_to_remote_micro_vm_instance("192.168.0.1", 8080) # You can't just call a remote procedure locally.
vm.load_bundle(read_file("some_bundle.uir"))
func = vm.put_function("@main")
argc = vm.put_int(len(sys.args))
argv = vm.new_hybrid(len(sys.args))
... # populate argv here
stack = vm.new_stack(func, [argc, argv])
thread = vm.new_thread(stack) There are other ways to do things in addition to the Mu-Client API: Things can be done via function calls to Mu functions. In this sense, part of the API can be implemented as pre-exposed Mu functions. Those Mu functions are first-class Mu citizens and can do anything in Mu. For example, the following Mu function can be used to allocate char arrays, which used to be done by the
This call-return interface is not allowed to pass (traced) references between the C-Mu boundary. But since the Mu Spec does not define handle as any particular type, hence simple data types, such as integers, can be returned to the C program to refer to this heap object later. When working with the heap, it is probably easier to do things in the Mu IR than in C: For example, when copying a C char buffer to a Mu
|
Calling convention Sometimes it is necessary to specify the calling convention (such as Windows API functions). It will be necessary that the exposed function pointer is supposed to use the appropriate calling convention, too.
p.s. Mu functions still use internal calling conventions when calling each other. That is hidden from (and in fact unrelated to) the native world. |
Can this mu->native/native->mu interface implement JNI without also requiring Mu to implement C++-style native stack unwinding? JNI allows C programs to handle Java exceptions via It should be sufficient to only support simple native-to-mu and mu-to-native function calls in order to support this JNI-style mixed native-managed exception handling. The main reason is that JNI is not "light-weight" or "simple". There can be an intermediate layer between Java and C in JNI which handles all the exception-related things, such as how a Java exception can pass through native code and reach the next Java frame below. In other words, Mu can provide a minimum interface while the client provides much more. Take As another example, when calling from Java to C, Mu-based JNI never calls the C function directly, but use a wrapper which, after the C function returns, restore the result of Should Mu exceptions go though C frames if not caught above the C frames? It should be easy to brutally destroy C frames (without respecting C++-level destructors. Sorry, C++ and embedded SpiderMonkey or V8). The worst consequence is the If the native program is required to be aware of Mu exceptions, then there should be wrappers, but I assume it will be rare. Though the Itanium C++ exception ABI is "zero-cost", cross-language calls are already non-zero-cost. I don't think cross-language exceptions can be. However, I haven't found existing runtimes that do such "brute-force" unwinding. (See below) LuaJIT claims that it may do this, but I have not observed such behaviour. Can OSR pop out C frames, too? As long as it is brutal, it should be easy. It can use the same mechanism as exception handling to unwind the stack. Existing languages/runtimesJVM: JNI: Throw() and ThrowNew() return 0 on success, but does not unwind the stack. Java exceptions from CPython: ctypes: Basically the same approach as JNI. Python exceptions are stored in a global variable (like GIL). If Python calls C calls Python, and Python raises an exception, then it does not unwind the stack, but return to C as if it just returned (with a garbage return value). The control flow goes on in C until returning to Python, when the exception in the global variable is discovered by the CPython interpreter. In the previous two approaches, C++ destructors are still executed because it does not consider any Java/Python exceptions as thrown, and stack unwinding never occurred. They pretend they are in the normal control flow (I think this may be dangerous because errors silently flows into C++ as a meaningless garbage return value). LuaJIT: The documentation mentioned the possibility of forced stack unwinding, but when I tried, if Lua calls C calls Lua, and Lua calls error("msg"), then C++ destructors are still executed. JVM: JNA (Java Native Access): When Java calls C calls Java, and Java throws an exception, then C receives 0 as the return value and continues normally. JNA logs the exception thrown from Java, but is not propagated back to the lower Java frame. JVM: JNR (Java Native Runtime): Like JNA, JNR does not unwind the stack, but returns 0 to C and continues normally. But JNR propagates the Java exception to the lower Java frame after returning from the native function. According to JNA's documentation: "A callback (the upper Java frame, written in Java, called by C) should generally never throw an exception, since it doesn't necessarily have an encompassing Java environment to catch it. Any exceptions thrown will be passed to the default callback exception handler." Julia: Copies the whole stack and (ab)uses |
Overview
Rationale
Some existing C libraries or system interfaces use call-back functions, i.e. user-provided function pointers which are called by C or system libraries. Mu should provide appropriate mechanisms to interface with those libraries.
This is part of the (unsafe) native interface. See super issue: #24
Exposing appropriate Mu functions as C-style function pointers
"Appropriate" Mu functions must only use the following types as their parameter types or return types:
int<n>
,float
,double
,vector<T>
,ptr<T>
orstruct
types whose components are these types. In the case ofptr<T>
,T
can also bearray<T n>
orhybrid<F V>
whereT
,F
andV
are one of the above types. In other words, (traced) references and Mu-specific opaque types are not allowed.The Mu ABI will be designed to be compatible with the C calling convention as defined by the platform ABI.
way 1: (simple) Mu functions are declared with the optional
WITH_FP
clauses to create their associated C-style function pointers. For example:With the above definitions,
@some_func
has typefunc<@sig>
, which is a Mu function reference value.@fp_some_func
has typefuncptr<@sig>
, which is a C-style function pointer. Similarly@other_func
is afunc<@sig2>
, while@fp_other_func
is afuncptr<@sig2>
.DEFAULT
is the calling convention.@COOKIE
is a "cookie" (see way 2 below).The Mu IR program or the API can pass the function pointer to the native program. When called, the Mu function will run and return its return value to the native caller.
way 2: (complex) Mu functions are exposed with a run-time invocation of a Mu instruction or a Mu API message.
Format:
EXPOSE_MU_FUNC
<
sig>
mufunc cookieThe resulting fp has type
funcptr<sig>
and can be called from C. A function can be exposed multiple times, and the resulting function pointers are mutually inequal. The cookie is anint<64>
value associated to the resulting function pointer. If a Mu function is called through a particular function pointer, a special instructionNATIVE_COOKIE
will return the associated cookie value.Example:
Both
%fp1
and%fp2
have typefuncptr<@sig>
. But if the Mu fucntion@some_func
is called from C via%fp1
, theNATIVE_COOKIE
instruction will return@some_int64_value
. If called via%fp2
, thenNATIVE_COOKIE
returns@other_int64_value
, instead.Contexts necessary for Mu functions to run
Even if a Mu function is exposed to the native program as a
functpr<sig>
, some contexts must be set up so that the Mu function can make use of Mu-specific features. These include:stack
value (the opaque reference to the current stack). This is necessary for swap-stack.Similar to the JNI's "attaching a native thread to the JVM", Mu will also require attaching Mu contexts to a native thread before any exposed Mu function pointers can be called.
If the native program is executed because some Mu program called the native function through the native interface (via
CCALL
), the context is already set up and the C program can safely call back to Mu.Mixed native/Mu stacks
With the possibility of both C-to-Mu and Mu-to-C calling, a stack may have mixed C or Mu frames. It has some implications for stack introspection and exception handling. Possible approaches are:
The text was updated successfully, but these errors were encountered: