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

feat: Support closures for imported functions #882

Closed
wants to merge 27 commits into from

Conversation

Hywan
Copy link
Contributor

@Hywan Hywan commented Oct 17, 2019

Reboot of #219:

  • runtime-core: Support closures
    The trampoline and the invoke functions take a new pointer to a function
    environment (*mut vm::FuncEnv).
    The function environment vm::FuncEnv is an opaque pointer that can
    be of 2 kinds: Either a closure environment pointer, or vm::Ctx.
  • clif-backend: Support first point,
  • llvm-backend: Support first point,
  • singlepass-backend: Support first point,
  • win-exception-handler: The C part must be updated.

The trampoline and the invoke function takes a pointer to a function
environment (`*mut vm::FuncEnv`) instead of a context (`*mut
vm::Ctx`).

The function environment `vm::FuncEnv` is an opaque pointer that can
be of 2 kinds: Either a closure environment pointer, or `vm::Ctx`.
@Hywan Hywan added 🎉 enhancement New feature! 📦 lib-compiler-cranelift About wasmer-compiler-cranelift 📦 lib-deprecated About the deprecated crates 🧪 tests I love tests 📚 documentation Do you like to read? 📦 lib-compiler-llvm About wasmer-compiler-llvm 📦 lib-compiler-singlepass About wasmer-compiler-singlepass labels Oct 17, 2019
@Hywan Hywan self-assigned this Oct 17, 2019
@Hywan Hywan changed the title feat(runtime-core) Support closures for imported functions. feat: Support closures for imported functions Oct 17, 2019
This patch makes the crate compatible with previous commits on
`runtime-core`.

Notably: The `crate::trampoline::Trampoline` type is removed since
`wasmer_runtime_core::typed_func::Trampoline` exists.
@Hywan
Copy link
Contributor Author

Hywan commented Oct 17, 2019

Collateral damage: #885

…mpoline`.

This patch uses the definition of `Trampoline` given in `runtime-core`
instead of copying it.

This patch also makes the C code more clear regarding what is
opaque. `wasmer_instance_context_t` is also renamed to `funcenv_t` to
avoid confusion with the Wasmer runtime C API.
@Hywan
Copy link
Contributor Author

Hywan commented Oct 17, 2019

Collateral damage: #886

Hywan added 6 commits October 18, 2019 00:11
…finitions.

This patch updates the LLVM backend to the previous commits that
update `wasmer-runtime-core` to support closures as imported
functions. The definitions of `Trampoline` and `Invoke` have
changed. Use the definitions from `runtime-core` instead of copying
it.
…oke definitions.

This patch updates the Singlepass backend to the previous commits that
update `wasmer-runtime-core` to support closures as imported
functions. The definitions of `Trampoline` and `Invoke` have
changed. Use the definitions from `wasmer-runtime-core` instead of
copying it.
bors bot added a commit that referenced this pull request Oct 30, 2019
916: feat(runtime-core-tests) Introduce the new `wasmer-runtime-core-tests` crate r=Hywan a=Hywan

Extracted from #882.

This non-publishable new crate contains a test suite for the
`wasmer-runtime-core` crate. So far, the test suite is rather small,
but it aims to be extended in a close future.

Co-authored-by: Ivan Enderlin <[email protected]>
bors bot added a commit that referenced this pull request Oct 31, 2019
915: fix(runtime-core) Share the definition of `Trampoline` across all the backends r=Hywan a=Hywan

Extracted from #882.

This patch updates all the backends to use the definition of
`Trampoline` as defined in the `wasmer_runtime_core::typed_func`
module. That way, there is no copy of that type, and as such, it is
easier to avoid regression (a simple `cargo check` does the job).

This patch also formats the `use` statements in the updated files.


Co-authored-by: Ivan Enderlin <[email protected]>
bors bot added a commit that referenced this pull request Oct 31, 2019
917: feat(runtime-core) Host function without a `vm::Ctx` argument r=Hywan a=Hywan

Extracted from #882.
~Includes #916. Must not be merged until #916 lands in `master`.~
~If you want to compare only the different commits, see https://github.com/Hywan/wasmer/compare/feat-runtime-core-tests...Hywan:feat-runtime-core-host-function-without-vmctx?expand=1.~

This patch updates the `ExternalFunction` implementation to support host functions (aka imported functions) without having an explicit `vm::Ctx` argument.

It is thus possible to write:

```rust
fn add_one(x: i32) -> i32 {
    x + 1
}

let import_object = imports! {
    "env" => {
        "add_one" => func!(add_one);
    },
};
```

The previous form is still working though:

```rust
fn add_one(_: &mut vm::Ctx, x: i32) -> i32 {
    x + 1
}
```

The backends aren't modified. It means that the pointer to `vm::Ctx` is still inserted in the stack, but it is not used when the function doesn't need it.

We thought this is an acceptable trade-off.

About the C API. It has not check to ensure the type signature. Since the pointer to `vm::Ctx` is always inserted, the C API can digest host functions with or without a `vm::Ctx` argument. The C API uses [the `Export` + `FuncPointer` API](https://github.com/wasmerio/wasmer/blob/cf5b3cc09eff149ce415bd81846d84b2d2cdf3a3/lib/runtime-c-api/src/import/mod.rs#L630-L653) instead of going through the `Func` API (which is modified by this API), which is only possible since the C API only receives an opaque function pointer. Conclusion is: We can't ensure anything on the C side, but it will work with or without the `vm::Ctx` argument in theory.

Co-authored-by: Ivan Enderlin <[email protected]>
@Hywan Hywan closed this Nov 4, 2019
bors bot added a commit that referenced this pull request Nov 12, 2019
925: feat(runtime-core) Support closures with a captured environment as host functions r=Hywan a=Hywan

Reboot of #882 and #219.

For the moment, host functions (aka imported functions) can be regular function pointers, or (as a side-effect) closures without a captured environment. This PR extends the support of host functions to closures with a captured environment. This is required for many other features (incl. the Python integration, the Ruby integration, WebAssembly Interface Types [see #787], and so on).

This PR is the culmination of previous ones, notably #915, #916 and #917. 

### General idea

The user-defined host function is wrapped inside a `wrap` function. This wrapper function initially receives a `vm::Ctx` as its first argument, which is passed to the host function when necessary. The patch keeps this behavior but it comes from `vm::FuncCtx`, which is a new structure. A `vm::FuncCtx` is held by `vm::ImportedFunc` such as:

```rust
#[repr(C)]
pub struct ImportedFunc {
    pub(crate) func: *const Func,
    pub(crate) func_ctx: NonNull<FuncCtx>,
}
```

where `vm::FuncCtx` is:

```rust
#[repr(C)]
pub struct FuncCtx {
    pub(crate) vmctx: NonNull<Ctx>,
    pub(crate) func_env: Option<NonNull<FuncEnv>>,
}
```

where `vm::FuncEnv` is:

```rust
#[repr(transparent)]
pub struct FuncEnv(pub(self) *mut c_void);
```

i.e. a raw opaque pointer.

So the wrapper function of a host function receives a `vm::Ctx`, which is used to find out the associated `FuncCtx` (by using the import backing), which holds `vm::FuncEnv`. It holds a pointer to the closure captured environment.

### Implementation details

#### How to get a pointer to a closure captured environment

A closure with a captured environment has a memory size greater than zero. This is how we detect it:

```rust
if mem::size_of::<Self>() != 0 { … }
```

To get a pointer to its captured environment, we use this statement:

```rust
NonNull::new(Box::into_raw(Box::new(self))).map(NonNull::cast)
```

(in `typed_func.rs`, in the `wrap` functions).

To reconstruct the closure based on the pointer, we use this statement:

```rust
let func: &FN = {
    let func: NonNull<FN> = func_env.cast();
    &*func.as_ptr()
};
```

That's basically how it works. And that's the core idea of this patch.

As a side effect, we have removed an undefined behavior (UB) in 2 places: The `mem::transmute(&())` has been removed (it was used to get the function pointer of `FN`). The transmute is replaced by `FuncEnv`, which provides a unified API, erasing the difference between host functions as closures with a captured environment, and host functions as function pointer. For a reason I ignore, the UB wasn't showing himself until this PR and a modification in the Singlepass backend. But now it's fixed.

#### Impact on `Backing`

After the modification on the `typed_func` and the `vm` modules, this is the other core idea of this patch: Updating the `backing` module so that `vm::ImportedFunc` replaces `vm::Ctx` by `vm::FuncCtx`.

When creating `vm::ImportedFunc`, a new `vm::FuncCtx` is created and its pointer is used. We are purposely leaking `vm::FuncCtx` so that the pointer is always valid. Hence the specific `Drop` implementation on `ImportBacking` to dereference the pointer, and to drop it properly.

#### Impact on the backends

Since the structure of `vm::ImportedFunc` has changed, backends must be updated. We must deref `FuncCtx` to reach its `vmctx` field.

#### Impact on `Instance`

Because `vm::ImportedFunc` has changed, it has a minor impact on the `instance` module, nothing crazy though.

Co-authored-by: Ivan Enderlin <[email protected]>
bors bot added a commit that referenced this pull request Nov 12, 2019
925: feat(runtime-core) Support closures with a captured environment as host functions r=Hywan a=Hywan

Reboot of #882 and #219.

For the moment, host functions (aka imported functions) can be regular function pointers, or (as a side-effect) closures without a captured environment. This PR extends the support of host functions to closures with a captured environment. This is required for many other features (incl. the Python integration, the Ruby integration, WebAssembly Interface Types [see #787], and so on).

This PR is the culmination of previous ones, notably #915, #916 and #917. 

### General idea

The user-defined host function is wrapped inside a `wrap` function. This wrapper function initially receives a `vm::Ctx` as its first argument, which is passed to the host function when necessary. The patch keeps this behavior but it comes from `vm::FuncCtx`, which is a new structure. A `vm::FuncCtx` is held by `vm::ImportedFunc` such as:

```rust
#[repr(C)]
pub struct ImportedFunc {
    pub(crate) func: *const Func,
    pub(crate) func_ctx: NonNull<FuncCtx>,
}
```

where `vm::FuncCtx` is:

```rust
#[repr(C)]
pub struct FuncCtx {
    pub(crate) vmctx: NonNull<Ctx>,
    pub(crate) func_env: Option<NonNull<FuncEnv>>,
}
```

where `vm::FuncEnv` is:

```rust
#[repr(transparent)]
pub struct FuncEnv(pub(self) *mut c_void);
```

i.e. a raw opaque pointer.

So the wrapper function of a host function receives a `vm::Ctx`, which is used to find out the associated `FuncCtx` (by using the import backing), which holds `vm::FuncEnv`. It holds a pointer to the closure captured environment.

### Implementation details

#### How to get a pointer to a closure captured environment

A closure with a captured environment has a memory size greater than zero. This is how we detect it:

```rust
if mem::size_of::<Self>() != 0 { … }
```

To get a pointer to its captured environment, we use this statement:

```rust
NonNull::new(Box::into_raw(Box::new(self))).map(NonNull::cast)
```

(in `typed_func.rs`, in the `wrap` functions).

To reconstruct the closure based on the pointer, we use this statement:

```rust
let func: &FN = {
    let func: NonNull<FN> = func_env.cast();
    &*func.as_ptr()
};
```

That's basically how it works. And that's the core idea of this patch.

As a side effect, we have removed an undefined behavior (UB) in 2 places: The `mem::transmute(&())` has been removed (it was used to get the function pointer of `FN`). The transmute is replaced by `FuncEnv`, which provides a unified API, erasing the difference between host functions as closures with a captured environment, and host functions as function pointer. For a reason I ignore, the UB wasn't showing himself until this PR and a modification in the Singlepass backend. But now it's fixed.

#### Impact on `Backing`

After the modification on the `typed_func` and the `vm` modules, this is the other core idea of this patch: Updating the `backing` module so that `vm::ImportedFunc` replaces `vm::Ctx` by `vm::FuncCtx`.

When creating `vm::ImportedFunc`, a new `vm::FuncCtx` is created and its pointer is used. We are purposely leaking `vm::FuncCtx` so that the pointer is always valid. Hence the specific `Drop` implementation on `ImportBacking` to dereference the pointer, and to drop it properly.

#### Impact on the backends

Since the structure of `vm::ImportedFunc` has changed, backends must be updated. We must deref `FuncCtx` to reach its `vmctx` field.

#### Impact on `Instance`

Because `vm::ImportedFunc` has changed, it has a minor impact on the `instance` module, nothing crazy though.

Co-authored-by: Ivan Enderlin <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
📚 documentation Do you like to read? 🎉 enhancement New feature! 📦 lib-compiler-cranelift About wasmer-compiler-cranelift 📦 lib-compiler-llvm About wasmer-compiler-llvm 📦 lib-compiler-singlepass About wasmer-compiler-singlepass 📦 lib-deprecated About the deprecated crates 🧪 tests I love tests
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant