I have a personal project, I want to write a JIT compiler/runtime in Rust (well, the language is not that relevant). I'm thinking about using a technique where the code is interpreted first and then JITted only when the runtime decides it is time to JIT (whatever that means).
This imposes a certain problem. How exactly interpreted and JITted code interacts with each other? Here's what I understand so far:
An interpreted function would look like this:
fn handle_call(context: &mut Context) { let current_fn = context.current_fn(); for instr in current_fn.instructions() { match instr { // do something } } } so in Rust I have a single function, that is called with different context, depending on what the interpreted function we are running at the moment.
It is easy to imagine that if I find a CALL instruction, then I retrieve the corresponding data, create new Context object and make (recursive) call to handle_call function.
But how do I call a JITted function? And how will a JITted function call interpreted function? Here are two options I can see:
JITted functions follow the same signature, they accept
&mut Contextobjects. This makes things very simple, since now calls between all functions are just calls through pointers. But I don't like this solution, since this involves potentially big overhead of building, passing around and usingContextobjects. For example if our function accepts ani32, then it can be passed in registers. What is worse ARGS have to be packed intoContextobject, and since we don't know how many of them there will be, this can mean a potential heap allocation per call. And this is a huge overhead.All functions, JITted or not, are treated the same way as low level, machine code functions. So JITted code simply emits instructions corresponding to some calling convention. While interpreted code now has to transform recursive call to
handle_callinto an appropriate machine code call. But how do I do that? I don't know the signature at compile time (as in: compiler's compile time), so how can I generate an arbitrary call? The only thing I can think of is to generate machine code on the fly whenCALLinstruction is seen.On the other hand how do I call interpreted function from JITted one? I cannot use a single
handle_callto handle all calls. Now I need to generate a wrapper for each function in my language that does the opposite to what I've mentioned earlier: it takes arguments passed through some low level calling convention, packs them intoContextobject and then callshandle_callwith it.I'm not worried about performance of this process, after all it is JITted code that is supposed to be optimized, not the interpreted code. What I'm worried is that this already requires JITting of at least some code. Originally I though that I can separate interpretter from JITter, but with this approach I have to use JITter at least partially. Which is a significant complication. Not only complication, I thought that I can use interpreter on platforms where I don't have JITter implemented. With this approach I cannot.
So am I forced to sacrifice performance here? Or JIT (at least partially) from the beginning and sacrifice simplicity and modularity? How do tiered JITters like Java (see here and here) do that? What am I missing here?