Creating DLL source at runtime and linking to it

I am building a rust library that will create source code. The original library will then compile and link to the source code.

  1. Are there any examples of doing this sort of thing ?

  2. Is it better to create Rust or C source for the code that will create the dll library that will link to the original library ?

How do you want to generate the DLL/shared object? It depends. Handwritting it is practically impossible for a human, so you'd end up generating some Rust crate first and then targetting a shared object/DLL.

A shared object crate

Some ChatGPT information:

A Rust crate representing a shared object should include this setting in the Cargo manifest (Cargo.toml):

[lib]
crate-type = ["cdylib"]

This crate can then export functions:

#[no_mangle]
pub extern "C" fn f() {
}

Loading a DLL

You may want the libloading crate for loading the generated DLL.

Build script

If you're working with generating DLLs from a build script, then beware the build script will only rerun depending on certain conditions (env var changed, directory changed...)

That doesn't make any sense at all. You can t statically link a dynamic library. Only dynamically link it.

2 Likes

You're right... fixed...

It seems to me that creating a crate for the dll is not reasonable for the case I am interested in. The original rust library will record the numerical operations for a function that the user chooses. The rust library will then determine the numerical operations to compute derivatives of the function. (The actual possible cases are more complicated, but I think this case describes the problem.)

Experience in C++ has shown that, if the derivative needs to be calculated for many different argument values, generating C code for the derivative, and linking it to the library is faster than interpreting the numerical operations for the derivative. The user often never sees the source code for the dll (it is generated by a program). In addition, there can be just a few, or many, derivative functions that get included in the dll (at the users choice).

Perhaps this can be done with an extra crate, that would also be distributed with the original crate, and that would get added to by the original crate. Is there an example of this ?

Here is an example of what I was looking for:

Cargo.toml

[package]
name = "package"
version = "0.0.0"
edition = "2024"

[dependencies]
libloading = "0.8"

main.rs

use std::path::Path;
use std::process::Command;
use std::fs::write;

fn create_my_lib() {
    let source = "
#[no_mangle]
pub extern \"C\" fn add_two_i32(a : i32, b : i32) -> i32 {
    a + b
}
    ";
    write( "build/my_lib.rs", source ).expect( "Cannot create build/my_lib.rs" );
}

fn call_add_two_i32(left : i32, right : i32) -> i32 {
    let result : i32;
    unsafe {
        let lib = libloading::Library::new("build/libmy_lib.so").
            expect("cannot find my_lib.so");
        let func : libloading::Symbol<
            unsafe extern "C" fn(left : i32, right : i32) -> i32
        > = lib.get( b"add_two_i32" ).
            expect( "Cannot find add_two_i32");
        result = func(left, right);
    }
    result
}

fn main()
{   //
    // build
    let build = Path::new("build");
    if ! build.is_dir() {
        Command::new("bash")
            .arg( "-c" )
            .arg( "mkdir build" )
            .output()
            .expect("mkdir build failed");
    }
    //
    // build/my_lib.rs
    create_my_lib();
    //
    // build/my_lib.so
    Command::new("bash")
        .arg( "-c" )
        .arg( "rustc --crate-type dylib build/my_lib.rs -o build/libmy_lib.so")
        .output()
        .expect("compile command failed");
    //
    let sum = call_add_two_i32(20 as i32, 30 as i32);
    println!("sum 20 + 30 = {}", sum);
}

Output:

>cargo run
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/package`
sum 20 + 30 = 50

It appears that one can remove extern \"C\" and unsafe extern "C" from the example above and it still works. I think this is because the version of rustc is the same for the compile of the dll library and the main program.

While you can actually make some pretty darn fast interpreters with clever tricks, this is pretty reasonable a use for dynamic code generation, sure.

That said, to me this sounds more like a use-case for JIT? I wouldn't want pull in the complexity and portability issues of a compiler toolchain and dynamic linking unless I really needed either the ability to do really aggressive optimization or compiling against platform APIs, and this is both purely numerical and highly dynamic.

There's a few JIT libraries out there, dynasm seems to be a pretty well supported and popular one due to it's use by wasmer, but you are writing platform dependant assembly directly. Your example might then look like:

use anyhow::Result;
use dynasmrt::{DynasmApi, dynasm};

fn main() -> Result<()> {
    let mut ops = dynasmrt::x64::Assembler::new()?;

    let add = ops.offset();
    // you can use dynamic ops too, or directly reference
    // rust values and functions.
    dynasm!(ops
        ; .arch x64
        ; mov eax, edi // a
        ; add eax, esi // b
        ; ret
    );

    ops.commit()?;
    let buf = ops.finalize().expect("should have no executors already");
    let add: extern "sysv64" fn(a: i32, b: i32) -> i32 =
        unsafe { std::mem::transmute(buf.ptr(add)) };
    let result = add(32, -75);
    assert_eq!(result, -43);

    Ok(())
}

Obviously, directly writing assembly is a bit rough! Lifting up to WASM might make more sense?

But if you are comfortable enough generating sources and compiling them, I don't think it's too crazy either - it's basically what Unreal Engine's live-reloading does, for example. Just be careful if you're loading and unloading things a lot as many platforms don't actually clean up previous libraries for annoying compatibility reasons.

Is your point here is that one would save the time to compile the rust code ? In the C++ case for very large functions, this becomes important.

Any JIT is going to be massively faster than an optimized C++ build, sure, and that can be important, but it's not the main reason I'd be concerned about pulling in a compiler toolchain as a dependency.

It's about the general concern I have that it's the sort of thing that works well in quick prototypes and examples, but falls over on weird and hard to diagnose ways depending on where it's running. Compilers and dynamic linking both have very dark weird corners that you can trip into quite easily doing something like this.

To be clear, you can do this, and people have successfully, and it's not like JIT (your own or using an existing implementation) doesn't have it's own issues, it's just I'd personally feel more comfortable dealing with those problems.

If you're interested in giving it a try but you're not confident on how to generate assembly, a quick hack to get decent assembly instructions is to use compiler explorer to see what sequences GCC and clang generate, it tends to be pretty simple for very short functions, and you can just stitch those together for a very good (10-100x faster is common) win over a simple interpreter.

Your main concern in terms of performance then is going to be "register allocation", that is, picking when and what to put into the registers so you're not continuously pulling things into and out of the stack, but getting rid of the indirection and branching is the main win for generating code over an interpreter, the specific instructions tend to not be as important.

There's a deep rabbit hole here of getting into optimizing compiler design that can both be pretty scary (mostly because everything has insane names) and a huge timesink, but it can be quite fun after you figure out what all the crazy names actually mean ("global value numbering" just means hashing expressions so you're not repeating evaluations, "dominator graphs" are just the blocks of code that had to have run to get to somewhere, for example)

I have added your JIT idea to the wish list for the library I am working on; see