Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC Host yielding API #2219

Closed
wants to merge 26 commits into from
Closed

RFC Host yielding API #2219

wants to merge 26 commits into from

Conversation

kaimast
Copy link

@kaimast kaimast commented Mar 29, 2021

Hi,

This is not intended to be merged into master yet, but I am curious about your thoughts on the approach. Essentially this pull request tries to fix/implement #1127 using the async-wormhole crate.

After enabling the async feature and you can call a function like so:

let instance = Instance::new(&module, import_object);

let stack = switcheroo::stack::EightMbStack;
instance.call_with_stack("my_func", stack);

This then executes the function in the userspace thread. You can yield host calls by exposing the Yielder to your WasmerEnv.

use wasmer::{LazyInit, Yielder};

#[ derive(WasmEnv) ]
sturct MyEnv {
     #[wasmer(yielder)]
      yielder: LazyInit<Yielder>
}

And then in the function call you can access the Yielder (which is just a clonable wrapper around async-wormhole's API).

fn my_host_func(env: &MyEnv) {
     let yielder = env.yielder.get_ref().unwrap().get();

     yielder.async_syspend(async {
             my_async_func().await;
     });
}

Copy link
Contributor

@Hywan Hywan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I salute the amount of work to make all that working correctly! Thanks!

I'll need some times to review this PR carefully and to think about the feature design exactly :-). I reckon the feature is great, but it will take time to evaluate the design/the approach.

Copy link
Contributor

@MarkMcCaskey MarkMcCaskey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting PR, thanks for going through and doing it. I don't understand how it all works yet, I'll probably have to look into what async-wormhole is doing to figure that out.

It's nice to have a proof of concept for this. I'm not currently well-informed enough to know what trade-offs this approach makes and if it's the right set of trade-offs for wasmer, but it seems minimally invasive...

Though changes like this in a system as complex as a Wasm VM can have a lot of side effects that we'll need to think through as well. I'll try to follow up on this more later once I've done some more research


#[ cfg(feature="async") ]
/// Set the thread-specific yielder
fn set_yielder(&mut self, _: *const std::ffi::c_void) {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probably needs to be unsafe and have a # Safety section describing how to correctly call and/or implement this function

#[ cfg(feature="async") ]
let (host_env, metadata) = {
let host_env_set_yielder_fn = |env_ptr, yielder_ptr| {
let env: &mut Env = unsafe { std::mem::transmute(env_ptr) };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's nice to be super explicit about what exactly is being transmuted from what to what to make bugs and intent more obvious. I'd prefer all transmute being transmute::<A,B> with explicit types whenever possible.

Comment on lines 70 to 116
#[ cfg(feature="async") ]
fn build_export_function_metadata<Env>(
env: Env,
import_init_function_ptr: for<'a> fn(
&'a mut Env,
&'a crate::Instance,
) -> Result<(), crate::HostEnvInitError>,
host_env_set_yielder_fn: fn(*mut std::ffi::c_void, *const std::ffi::c_void)
) -> (*mut std::ffi::c_void, ExportFunctionMetadata)
where
Env: Clone + Sized + 'static + Send + Sync,
{
let import_init_function_ptr = Some(unsafe {
std::mem::transmute::<fn(_, _) -> Result<(), _>, fn(_, _) -> Result<(), _>>(
import_init_function_ptr,
)
});
let host_env_clone_fn: fn(*mut std::ffi::c_void) -> *mut std::ffi::c_void = |ptr| {
let env_ref: &Env = unsafe {
ptr.cast::<Env>()
.as_ref()
.expect("`ptr` to the environment is null when cloning it")
};
Box::into_raw(Box::new(env_ref.clone())) as _
};
let host_env_drop_fn: fn(*mut std::ffi::c_void) = |ptr| {
unsafe { Box::from_raw(ptr.cast::<Env>()) };
};
let env = Box::into_raw(Box::new(env)) as _;

// # Safety
// - All these functions work on all threads
// - The host env is `Send`.
let metadata = unsafe {
ExportFunctionMetadata::new(
env,
import_init_function_ptr,
host_env_clone_fn,
host_env_set_yielder_fn,
host_env_drop_fn,
)
};

(env, metadata)
}

#[ cfg(not(feature="async")) ]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer having the cfg logic internal to the original function rather than having 2 copies just because it's so precise, dangerous, and error-prone. The reason this function exists in the first place is to try to avoid duplicating this kind of code.

#[ cfg(feature="async") ]
let (host_env, metadata) = {
let host_env_set_yielder_fn = |env_ptr, yielder_ptr| {
let env: &mut Env = unsafe { std::mem::transmute(env_ptr) };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Comment on lines 11 to 23
impl Yielder {
/// Create a new instance from a raw pointer to a yielder
pub fn new(inner: *const std::ffi::c_void) -> Self {
Self{ inner }
}

/// Get the `AsyncYielder`
pub fn get(&self) -> &mut AsyncYielder<Result<Box<[Val]>, RuntimeError>> {
let yielder: &mut AsyncYielder<Result<Box<[Val]>, RuntimeError>>= unsafe { std::mem::transmute(self.inner) };

yielder
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either new needs to be unsafe or get needs to be unsafe (or possibly both)

Comment on lines 181 to 185
let res = func.call_wasm(&wasm, params, &mut results);

if let Err(e) = res {
return Err(e);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let res = func.call_wasm(&wasm, params, &mut results)?;

Comment on lines 266 to 270
let instance_handle = self.artifact.instantiate(
self.store.tunables(),
resolver,
Box::new(()),
)?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like unrelated formatting changes, we use 1 behind latest stable (though I think we may not have updated to 1.50 in this repo yet), would be good to run that version of rustfmt on the code

Comment on lines 16 to 20
#[ cfg(feature="async") ]
Yielder {
identifier: Option<LitStr>,
span: Span,
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor nit (I haven't review all the derive macro changes yet) but we probably want to unconditionally parse this attribute and error if the feature isn't enabled or otherwise generate code that will provide a useful error message in that case. Don't worry about doing all that work now for this experimental PR, just writing this down for future reference.

@kaimast
Copy link
Author

kaimast commented Mar 30, 2021

Thanks for taking a look. I should also note that the commit history is kind of a mess right now and the pull request makes some unnecessary changes, mainly because I pulled these changes out of a larger branch.

@syrusakbary
Copy link
Member

syrusakbary commented Mar 31, 2021

Some thoughts on the API design.

I believe a more ergonomic API would be:

Call async function

let instance = Instance::new(&module, import_object);

// It can be EightMbStack or OneMbStack
// This call is optional (and it can be EightMbStack by default if not called)
store.set_stack(EightMbStack);

instance.call_async("my_func");

Function definition

We can allow functions to be defined async, and detect them with the Rust type system (automatically wrapping them).

async fn my_host_func(env: &MyEnv) {
    my_async_func().await;
}

I'm not super convinced of having the yielder in the Env, but I'm not super sure what are the alternatives. Thoughts?

Apart from this, we will need to add tests and benchmark the calls (similarly as we do benchmarking of other function calls)

@kaimast
Copy link
Author

kaimast commented Apr 30, 2021

Some thoughts on the API design.

I believe a more ergonomic API would be:

Call async function

let instance = Instance::new(&module, import_object);

// It can be EightMbStack or OneMbStack
// This call is optional (and it can be EightMbStack by default if not called)
store.set_stack(EightMbStack);

instance.call_async("my_func");

Function definition

We can allow functions to be defined async, and detect them with the Rust type system (automatically wrapping them).

async fn my_host_func(env: &MyEnv) {
    my_async_func().await;
}

I'm not super convinced of having the yielder in the Env, but I'm not super sure what are the alternatives. Thoughts?

Apart from this, we will need to add tests and benchmark the calls (similarly as we do benchmarking of other function calls)

I like these changes a lot. Will close this pull request for now and reopen once I have time to implement them.

Thanks for all the feedback!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants