Skip to content

Requesting response in regards to implementation of Orc engine. #603

@ErisianArchitect

Description

@ErisianArchitect

Hope I'm not bothering you too much.

I'm just wrapping up work on the Orc V1 implementation (it's taking longer than I thought), but I've come across some issues that I would like your feedback on before continuing on my implementation.

In the Orc V1 engine, the JIT Stack (basically the execution engine) takes ownership of modules that are added to it. This doesn't seem like a problem at first glance, because it's not too difficult to do this. Just pass the entire module to the Orc Engine and allow the orc engine to handle ownership. The problem comes with the lifetime of the Context, because the module will still be tied to the context that created it. That's why I think it would be a good idea to turn the context into a reference counted smart pointer wrapper, and give a clone of the context to the module to keep the context alive. Then when the module is added to the Orc Engine, the smart pointer to the context is moved into the Orc Engine's "keep alive" system. This could be made possible by placing ContextImpl into an Arc, and forcing Context to be !Send and !Sync by adding a PhantomData<*const ()> field. This could be changed later on so that Context could be thread-safe if need-be.

Another issue that I came across has to do with symbol resolution in the Orc Engine. As far as I'm aware, resolution goes like this: search external symbols in inverse order (LIFO) that modules were added, then when the symbol is not found in any of those modules, the user supplied symbol resolver for each module in LIFO order is searched. The goal for me is to make it so that there's a final fallback global symbol table for things to resolve to when all else fails. The solution that I've come up with to solve this problem is for the Orc Engine to create a dummy module that resolves to the global table for the express purpose of having a fallback lookup table. This dummy module would be added as soon as the engine is created.

Some other things aren't issues I needed feedback on, but I figured I would give you an update on.

I added lockfree_linked_list.rs to /src/. It's, as the name implies, a lock-free linked list. I use it in the Orc Engine implementation to make a thread-safe list to append to in order to keep lazy compile callbacks in memory. I added to the crate-level namespace because I figured that it might be useful in other parts of the crate, especially Orc2.

I also added listeners.rs, which is for JITEventListeners. I don't know if these event listeners are exclusive to Orc or not, so I just added the module to the crate-level namespace as a precaution. They didn't seem Orc specific since they aren't in the Orc namespace.

I created a neat FunctionAddress abstraction as well as a fn_addr! macro. This simplifies creating LLVMOrcTargetAddresses from from extern functions.

let add_addr0 = fn_addr!(jit_functions::add : (i32, i32) -> i32);
// optionally, you can include `fn`, `extern "C" fn`, or `unsafe extern "C" fn` before the parameter list:
let add_addr1 = fn_addr!(jit_functions::add : fn(i32, i32) -> i32);
let add_addr2 = fn_addr!(jit_functions::add : extern "C" fn(i32, i32) -> i32);
let add_addr3 = fn_addr!(jit_functions::add : unsafe extern "C" fn(i32, i32) -> i32);
// When the function has no arguments nor return type, you can exclude the signature.
let say_hello_addr = fn_addr!(jit_functions::say_hello);

The fn_addr macro makes it much easier to safely get a function address without allowing users to pass in raw values.
Here's another example from my sandbox:

extern "C" fn bar(num: i32) {
    println!("extern \"C\" fn bar({num})");
}

let func_addr = engine.create_lazy_compile_callback(|engine| {
    println!("Lazy Compile Callback was called.");
    fn_addr!(bar : (i32))
}).unwrap();

Here, you can see fn_addr in action, but you can also see how the creation of a lazy_compile callback works. The lazy compile callback is simply a trampoline function internal to the JIT stack that will call your provided function to lazily compile some function, then the address of the callback will be replaced with the address that you return. It's pretty neat, and it was a little complicated to make it work with a closure rather than a raw extern "C" function.

Lastly, I added an UnsafeOrcFn trait similar to the UnafeFunctionPointer trait. I even implemented it in a similar way. I just wanted to have 32 arguments for the Orc Engine so that there would be less limitation in the safe API. Ideally, I would like to create a custom proc macro for generating these parameterized implementations. That way the argument names for the call functions would not be PascalCase, and could instead be arg0: T0, arg1: T1, arg2: T2, arg3: T3, .... I didn't want to add a barely used proc macro to the crate without your blessing, though, so I didn't do so. And just in case you're curious, I tested compile times both with my 32 parameters macro and without it, and there was no discernible difference.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions