C++ and javascript bindings for rust code

I have some rust code, that I want to use from both c++ and javascript code. Currently, I have two separate crates that have the main rust crate as a dependency: one exposes c++ API, and the other exposes WASM API. But actually, these two crates are very similar to each other:

// cpp
#[no_mangle]
pub extern "C" fn do_something(foo: *const Foo) -> i32 {
    unsafe { &*foo }.do_something();
}

// wasm
#[wasm_bindgen]
pub fn do_something(foo: *const Foo) -> i32 {
    unsafe { &*foo }.do_something()
}

As you can see they differ only in attributes and use of the extern keyword.

I would like to have only one crate for both C++ and WASM and just compile it with different compiler options to get the C library or the WASM file.
I'm not sure how to do this in a better way. I do not see a way to do this with just conditional compilation. As for macros, I do not have any experience with that or even any knowledge beyond brief reading of the docs, but I can imagine doing something like:

macro_rules! export_func {
	($func_name:ident, $code:block) => {
		#[cfg(target_family = "wasm")]
		#[wasm_bindgen]
		fn $func_name() {
			$code
		}

		#[cfg(not(target_family = "wasm"))]
		#[no_mangle]
		pub extern "C" fn $func_name() {
			$code
		}
	};
}

export_func!(foo, {
	println!("hello world");
});

But adding function args will make it even uglier. Maybe there are better alternatives.

Also, I'm interested in generating some code in another language from rust sources, because the code that will be using this rust code is not in C++ or javascript, but in another language that is compiled to c++ or javascript, so I would like to have bindings to rust code in this language.

You could probably make the contents of the export_func macro a bit prettier, but it is probably the best bet unless you want to go for a proc-macro, which is a lot more complicated.

It’s not all that complicated though :slight_smile:

use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, parse_quote, ItemFn};

#[proc_macro_attribute]
pub fn export_func(args: TokenStream, input: TokenStream) -> TokenStream {
    assert!(args.is_empty()); // we want no macro arguments

    let function1 = parse_macro_input!(input as ItemFn);
    let mut function2 = function1.clone();

    assert!(function2.sig.abi.is_none()); // we don't want to replace existing "extern ..."

    function2.sig.abi = parse_quote!(extern "C");

    quote! {
        #[cfg(target_family = "wasm")]
        #[wasm_bindgen]
        #function1

        #[cfg(not(target_family = "wasm"))]
        #[no_mangle]
        #function2
    }
    .into()
}
[lib]
proc-macro = true

[dependencies]
quote = "1.0.23"
syn = { version = "1.0.107", features = ["full"] }
#[export_func]
fn do_something(foo: *const Foo) -> i32 {
    unsafe { &*foo }.do_something();
}

of course the remaining downside is that a proc-macro needs to live in a separate crate

3 Likes

Isn't it possible to find the current configuration during macro execution to generate only one function instead of two?

Also is it possible to write a file during proc macro execution to output all processed function names and their types?

The cfg will already eliminate one of them very quickly, before doing any analysis on it beyond a simple first parse; I don’t see much need for optimization here.

This would not be so straightforward, I believe. Here’s a relevant post with more information.

2 Likes