bytecodealliance / wamr-rust-sdk

Apache License 2.0
29 stars 9 forks source link

Binding generator #27

Closed AlixANNERAUD closed 1 month ago

AlixANNERAUD commented 4 months ago

Hello, in order to easily interface WAMR with Rust, I wrote "wamr-bindgen" macros that automatically write the necessary code (signature, binding function, conversions). At the moment, it is only a crude prototype, but it is functional in the case of simple types and references on composite types (generic types and composite types passed by value/returned are not supported). For the following standalone function :

#[function_bindgen]
fn test_function(_a: i32, _b: f32, _c: String, _d: &str, _e: i64, _f: u64, _g: i8) -> u8 {
    42
}

the generated code is :

const __WAMR_BINDGEN_TEST_FUNCTION_SIGNATURE: &'static str = "(if$$IIi)i";
#[no_mangle]
pub unsafe extern "C" fn __wamr_bindgen_test_function(
    __wamr_environment: wamr_sys::wasm_exec_env_t,
    _a: i32,
    _b: f32,
    _c: u32,
    _d: u32,
    _e: i64,
    _f: i64,
    _g: i32,
) -> i32 {
    let __wamr_instance = unsafe {
        wamr_sys::wasm_runtime_get_module_inst(__wamr_environment)
    };
    let _a = _a as i32;
    if !unsafe { wamr_sys::wasm_runtime_validate_app_str_addr(__wamr_instance, _c) } {
        {
            ::core::panicking::panic_fmt(format_args!("Invalid pointer"));
        }
    }
    let _c: *const char = unsafe {
        std::mem::transmute(
            wamr_sys::wasm_runtime_addr_app_to_native(__wamr_instance, _c),
        )
    };
    let _c = unsafe { std::ffi::CStr::from_ptr(_c as *const i8) }
        .to_str()
        .unwrap()
        .to_string();
    if !unsafe { wamr_sys::wasm_runtime_validate_app_str_addr(__wamr_instance, _d) } {
        {
            ::core::panicking::panic_fmt(format_args!("Invalid pointer"));
        }
    }
    let _d: *const char = unsafe {
        std::mem::transmute(
            wamr_sys::wasm_runtime_addr_app_to_native(__wamr_instance, _d),
        )
    };
    let _d = unsafe { std::ffi::CStr::from_ptr(_d as *const i8) }.to_str().unwrap();
    let _e = _e as i64;
    let _f = _f as u64;
    let _g = _g as i8;
    test_function(_a, _b, _c, _d, _e, _f, _g) as i32
}

and for an impl on a structure :

struct Test {}

#[impl_bindgen]
impl Test {
    fn test_function(
        &self,
        a: i32,
        b: f32,
        c: String,
        d: &str,
        e: i64,
        f: &u64,
        g: i8,
        h: u8,
        i: i16,
        j: u16,
        k: f64,
    ) -> u8 {
        42
    }
}

the generated code is :

const __WAMR_BINDGEN_TEST_FUNCTION_SIGNATURE: &'static str = "(*if$$I*iiiiF)i";
#[no_mangle]
pub unsafe extern "C" fn __wamr_bindgen_Test_test_function(
    __wamr_environment: wamr_sys::wasm_exec_env_t,
    __self: u32,
    a: i32,
    b: f32,
    c: u32,
    d: u32,
    e: i64,
    f: u32,
    g: i32,
    h: i32,
    i: i32,
    j: i32,
    k: f64,
) -> i32 {
    let __wamr_instance = unsafe {
        wamr_sys::wasm_runtime_get_module_inst(__wamr_environment)
    };
    if !unsafe {
        wamr_sys::wasm_runtime_validate_app_addr(
            __wamr_instance,
            __self,
            std::mem::size_of::<Test>() as u32,
        )
    } {
        {
            ::core::panicking::panic_fmt(format_args!("Invalid pointer"));
        }
    }
    let __self: &mut Test = unsafe {
        std::mem::transmute(
            wamr_sys::wasm_runtime_addr_app_to_native(__wamr_instance, __self),
        )
    };
    let a = a as i32;
    if !unsafe { wamr_sys::wasm_runtime_validate_app_str_addr(__wamr_instance, c) } {
        {
            ::core::panicking::panic_fmt(format_args!("Invalid pointer"));
        }
    }
    let c: *const char = unsafe {
        std::mem::transmute(
            wamr_sys::wasm_runtime_addr_app_to_native(__wamr_instance, c),
        )
    };
    let c = unsafe { std::ffi::CStr::from_ptr(c as *const i8) }
        .to_str()
        .unwrap()
        .to_string();
    if !unsafe { wamr_sys::wasm_runtime_validate_app_str_addr(__wamr_instance, d) } {
        {
            ::core::panicking::panic_fmt(format_args!("Invalid pointer"));
        }
    }
    let d: *const char = unsafe {
        std::mem::transmute(
            wamr_sys::wasm_runtime_addr_app_to_native(__wamr_instance, d),
        )
    };
    let d = unsafe { std::ffi::CStr::from_ptr(d as *const i8) }.to_str().unwrap();
    let e = e as i64;
    if !unsafe {
        wamr_sys::wasm_runtime_validate_app_addr(
            __wamr_instance,
            f,
            std::mem::size_of::<u64>() as u32,
        )
    } {
        {
            ::core::panicking::panic_fmt(format_args!("Invalid pointer"));
        }
    }
    let f: &u64 = unsafe {
        std::mem::transmute(
            wamr_sys::wasm_runtime_addr_app_to_native(__wamr_instance, f),
        )
    };
    let g = g as i8;
    let h = h as u8;
    let i = i as i16;
    let j = j as u16;
    Test::test_function(__self, a, b, c, d, e, f, g, h, i, j, k) as i32
}

Can I have any advice on that ?

lum1n0us commented 4 months ago

Thanks for contribution.

I noticed the PR is using APIs of wamr_sys in generated code which is not our suggestions. We are hoping people use wamr-rust-sdk APIs to keep their code safe and don't involved those unsafe blocks.

AlixANNERAUD commented 4 months ago

Here is the output for both example with new abstraction :

#[no_mangle]
pub unsafe extern "C" fn __wamr_bindgen_test_function(
    __wamr_environment: wamr_sys::wasm_exec_env_t,
    _a: i32,
    _b: f32,
    _c: u32,
    _d: u32,
    _e: i64,
    _f: i64,
    _g: i32,
) -> i32 {
    let __wamr_environment = wamr_rust_sdk::execution_environment::ExecutionEnvironment::from(
        __wamr_environment,
    );
    let __wamr_instance = __wamr_environment.get_instance();
    let _a = _a as i32;
    let _c = __wamr_instance.app_to_native_str(_c).to_string();
    let _d = __wamr_instance.app_to_native_str(_d);
    let _e = _e as i64;
    let _f = _f as u64;
    let _g = _g as i8;
    test_function(_a, _b, _c, _d, _e, _f, _g) as i32
}

and :

#[no_mangle]
pub unsafe extern "C" fn __wamr_bindgen_Test_test_function(
    __wamr_environment: wamr_sys::wasm_exec_env_t,
    __self: u32,
    a: i32,
    b: f32,
    c: u32,
    d: u32,
    e: i64,
    f: u32,
    g: i32,
    h: i32,
    i: i32,
    j: i32,
    k: f64,
) -> i32 {
    let __wamr_environment = wamr_rust_sdk::execution_environment::ExecutionEnvironment::from(
        __wamr_environment,
    );
    let __wamr_instance = __wamr_environment.get_instance();
    let __self: &Test = __wamr_instance.app_to_native_ref(__self);
    let a = a as i32;
    let c = __wamr_instance.app_to_native_str(c).to_string();
    let d = __wamr_instance.app_to_native_str(d);
    let e = e as i64;
    let f: &u64 = __wamr_instance.app_to_native_ref(f);
    let g = g as i8;
    let h = h as u8;
    let i = i as i16;
    let j = j as u16;
    Test::test_function(__self, a, b, c, d, e, f, g, h, i, j, k) as i32
}
lum1n0us commented 4 months ago
AlixANNERAUD commented 4 months ago
  • __wamr_environment.get_instance(). we prefer user using instance.rs instead of wamr C structure.

  • app_to_native_str. we prefer user doesn't be aware of it.

lum1n0us commented 3 months ago
  • Okay, but how would you suggest translating addresses from Wasm to native then?

I am not quite following why we need to operate address like C in Rust World?

AlixANNERAUD commented 3 months ago

I am not quite following why we need to operate address like C in Rust World?

How am I supposed to pass, for example, strings or arrays from WASM to the native and vice versa? (necessary for the serialization of complex structures).

lum1n0us commented 3 months ago

The whole idea is to keep user interfaces simple. We don't want to let APIs' users keep much concepts in their mind when programing with wamr-rust-sdk. Using a String as an example, it will be too much if they have to 1. move a string to wasm memory from host memory. 2. construct a WasmValue as a parameter of a wasm function. 3. most important thing, rust strings can be used directly because of encoding and \0.

In my mind, an ideal result is use one API, like WasmValue::String() only, to finish all previous tasks together.

AlixANNERAUD commented 3 months ago

The whole idea is to keep user interfaces simple. We don't want to let APIs' users keep much concepts in their mind when programing with wamr-rust-sdk. Using a String as an example, it will be too much if they have to 1. move a string to wasm memory from host memory. 2. construct a WasmValue as a parameter of a wasm function. 3. most important thing, rust strings can be used directly because of encoding and \0.

In my mind, an ideal result is use one API, like WasmValue::String() only, to finish all previous tasks together.

Ok but I'm struggling to see how you plan to implement WasmValue::String(). Besides wrapping it around a pointer returned by WASM to native, I don't quite see how it's possible without using address translation / raw pointers (I'm talking about the case where wasm calls a host function).

Furthermore, if you don't want to complicate the API for the end user, would it be possible to at least expose wamr-sys (pub use wamr-sys as sys;) to provide flexibility for users who desire it?

lum1n0us commented 3 months ago

without using address translation / raw pointers

Sorry for the mis-understand. address translation is still necessary in rust-sdk. But rust-sdk users shouldn't be awarded of that.