Skip to content
This repository was archived by the owner on Mar 24, 2022. It is now read-only.

Commit 3d8df65

Browse files
authored
Merge pull request #612 from bytecodealliance/cfallin/bounded-execution
Add a "bounded runtime" mode to guest execution.
2 parents 27f168b + ea3c239 commit 3d8df65

File tree

16 files changed

+771
-206
lines changed

16 files changed

+771
-206
lines changed

lucet-module/src/runtime.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,18 @@
44
#[repr(align(8))]
55
pub struct InstanceRuntimeData {
66
pub globals_ptr: *mut i64,
7-
pub instruction_count: u64,
7+
/// `instruction_count_bound + instruction_count_adj` gives the total
8+
/// instructions executed. We deconstruct the count into a signed adjustment
9+
/// and a "bound" because we want to be able to set a runtime bound beyond
10+
/// which we yield to the caller. We do this by beginning execution with
11+
/// `instruction_count_adj` set to some negative value and
12+
/// `instruction_count_bound` adjusted upward in compensation.
13+
/// `instruction_count_adj` is incremented as execution proceeds; on each
14+
/// increment, the Wasm code checks the sign. If the value is greater than
15+
/// zero, then we have exceeded the bound and we must yield. At any point,
16+
/// the `adj` value can be adjusted downward by transferring the count to
17+
/// the `bound`.
18+
pub instruction_count_adj: i64,
19+
pub instruction_count_bound: i64,
820
pub stack_limit: u64,
921
}

lucet-runtime/include/lucet_vmctx.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,10 @@ void *lucet_vmctx_get_func_from_idx(struct lucet_vmctx const *ctx, uint32_t tabl
4242
// Mostly for tests - this conversion is builtin to lucetc
4343
int64_t *lucet_vmctx_get_globals(struct lucet_vmctx const *ctx);
4444

45+
// Yield that is meant to be inserted by compiler instrumentation, transparent
46+
// to Wasm code execution. It is intended to be invoked periodically (e.g.,
47+
// every N instructions) to bound runtime of any particular execution slice of
48+
// Wasm code.
49+
void lucet_vmctx_yield_at_bound_expiration(struct lucet_vmctx const *ctx);
50+
4551
#endif // LUCET_VMCTX_H

lucet-runtime/lucet-runtime-internals/src/future.rs

Lines changed: 116 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
use crate::error::Error;
2-
use crate::instance::{InstanceHandle, RunResult, State, TerminationDetails};
2+
use crate::instance::{InstanceHandle, InternalRunResult, RunResult, State, TerminationDetails};
3+
use crate::module::FunctionHandle;
34
use crate::val::{UntypedRetVal, Val};
45
use crate::vmctx::{Vmctx, VmctxInternal};
56
use std::any::Any;
67
use std::future::Future;
78
use std::pin::Pin;
9+
use std::task::{Context, Poll};
810

911
/// This is the same type defined by the `futures` library, but we don't need the rest of the
1012
/// library for this purpose.
@@ -75,23 +77,38 @@ impl Vmctx {
7577
// Wrap the computation in `YieldedFuture` so that
7678
// `Instance::run_async` can catch and run it. We will get the
7779
// `ResumeVal` we applied to `f` above.
78-
self.yield_impl::<YieldedFuture, ResumeVal>(YieldedFuture(f), false);
80+
self.yield_impl::<YieldedFuture, ResumeVal>(YieldedFuture(f), false, false);
7981
let ResumeVal(v) = self.take_resumed_val();
8082
// We may now downcast and unbox the returned Box<dyn Any> into an `R`
8183
// again.
8284
*v.downcast().expect("run_async broke invariant")
8385
}
8486
}
8587

86-
/// This struct needs to be exposed publicly in order for the signature of a
87-
/// "block_in_place" function to be writable, a concession we must make because
88-
/// Rust does not have rank 2 types. To prevent the user from inspecting or
89-
/// constructing the inside of this type, it is completely opaque.
90-
pub struct Bounce<'a>(BounceInner<'a>);
88+
/// A simple future that yields once. We use this to yield when a runtime bound is reached.
89+
///
90+
/// Inspired by Tokio's `yield_now()`.
91+
struct YieldNow {
92+
yielded: bool,
93+
}
94+
95+
impl YieldNow {
96+
fn new() -> Self {
97+
Self { yielded: false }
98+
}
99+
}
91100

92-
enum BounceInner<'a> {
93-
Done(UntypedRetVal),
94-
More(BoxFuture<'a, ResumeVal>),
101+
impl Future for YieldNow {
102+
type Output = ();
103+
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
104+
if self.yielded {
105+
Poll::Ready(())
106+
} else {
107+
self.yielded = true;
108+
cx.waker().wake_by_ref();
109+
Poll::Pending
110+
}
111+
}
95112
}
96113

97114
impl InstanceHandle {
@@ -101,51 +118,59 @@ impl InstanceHandle {
101118
/// that use `Vmctx::block_on` and provides the trampoline that `.await`s those futures on
102119
/// behalf of the guest.
103120
///
121+
/// If `runtime_bound` is provided, it will also pause the Wasm execution and yield a future
122+
/// that resumes it after (approximately) that many Wasm opcodes have executed.
123+
///
104124
/// # `Vmctx` Restrictions
105125
///
106126
/// This method permits the use of `Vmctx::block_on`, but disallows all other uses of `Vmctx::
107127
/// yield_val_expecting_val` and family (`Vmctx::yield_`, `Vmctx::yield_expecting_val`,
108128
/// `Vmctx::yield_val`).
129+
pub async fn run_async<'a>(
130+
&'a mut self,
131+
entrypoint: &'a str,
132+
args: &'a [Val],
133+
runtime_bound: Option<u64>,
134+
) -> Result<UntypedRetVal, Error> {
135+
let func = self.module.get_export_func(entrypoint)?;
136+
self.run_async_internal(func, args, runtime_bound).await
137+
}
138+
139+
/// Run the module's [start function][start], if one exists.
109140
///
110-
/// # Blocking thread
111-
///
112-
/// The `wrap_blocking` argument is a function that is called with a closure that runs the Wasm
113-
/// program. Since Wasm may execute for an arbitrarily long time without `await`ing, we need to
114-
/// make sure that it runs on a thread that is allowed to block.
115-
///
116-
/// This argument is designed with [`tokio::task::block_in_place`][tokio] in mind. The odd type
117-
/// is a concession to the fact that we don't have rank 2 types in Rust, and so must fall back
118-
/// to trait objects in order to be able to take an argument that is itself a function that
119-
/// takes a closure.
120-
///
121-
/// In order to provide an appropriate function, you may have to wrap the library function in
122-
/// another closure so that the types are compatible. For example:
123-
///
124-
/// ```no_run
125-
/// # async fn f() {
126-
/// # let instance: lucet_runtime_internals::instance::InstanceHandle = unimplemented!();
127-
/// fn block_in_place<F, R>(f: F) -> R
128-
/// where
129-
/// F: FnOnce() -> R,
130-
/// {
131-
/// // ...
132-
/// # f()
133-
/// }
141+
/// If there is no start function in the module, this does nothing.
134142
///
135-
/// instance.run_async("entrypoint", &[], |f| block_in_place(f)).await.unwrap();
136-
/// # }
137-
/// ```
143+
/// All of the other restrictions on the start function, what it may do, and
144+
/// the requirement that it must be invoked first, are described in the
145+
/// documentation for `Instance::run_start()`. This async version of that
146+
/// function satisfies the requirement to run the start function first, as
147+
/// long as the async function fully returns (not just yields).
138148
///
139-
/// [tokio]: https://docs.rs/tokio/0.2.21/tokio/task/fn.block_in_place.html
140-
pub async fn run_async<'a, F>(
149+
/// This method is similar to `Instance::run_start()`, except that it bounds
150+
/// runtime between async future yields (invocations of `.poll()` on the
151+
/// underlying generated future) if `runtime_bound` is provided. This
152+
/// behaves the same way as `Instance::run_async()`.
153+
pub async fn run_async_start<'a>(
141154
&'a mut self,
142-
entrypoint: &'a str,
155+
runtime_bound: Option<u64>,
156+
) -> Result<(), Error> {
157+
if let Some(start) = self.module.get_start_func()? {
158+
if !self.is_not_started() {
159+
return Err(Error::StartAlreadyRun);
160+
}
161+
self.run_async_internal(start, &[], runtime_bound).await?;
162+
}
163+
Ok(())
164+
}
165+
166+
/// Shared async run-loop implementation for both `run_async()` and
167+
/// `run_start_async()`.
168+
async fn run_async_internal<'a>(
169+
&'a mut self,
170+
func: FunctionHandle,
143171
args: &'a [Val],
144-
wrap_blocking: F,
145-
) -> Result<UntypedRetVal, Error>
146-
where
147-
F: for<'b> Fn(&mut (dyn FnMut() -> Result<Bounce<'b>, Error>)) -> Result<Bounce<'b>, Error>,
148-
{
172+
runtime_bound: Option<u64>,
173+
) -> Result<UntypedRetVal, Error> {
149174
if self.is_yielded() {
150175
return Err(Error::Unsupported(
151176
"cannot run_async a yielded instance".to_owned(),
@@ -156,55 +181,55 @@ impl InstanceHandle {
156181
let mut resume_val: Option<ResumeVal> = None;
157182
loop {
158183
// Run the WebAssembly program
159-
let bounce = wrap_blocking(&mut || {
160-
let run_result = if self.is_yielded() {
161-
// A previous iteration of the loop stored the ResumeVal in
162-
// `resume_val`, send it back to the guest ctx and continue
163-
// running:
164-
self.resume_with_val_impl(
165-
resume_val
166-
.take()
167-
.expect("is_yielded implies resume_value is some"),
168-
true,
169-
)
170-
} else {
171-
// This is the first iteration, call the entrypoint:
172-
let func = self.module.get_export_func(entrypoint)?;
173-
self.run_func(func, args, true)
174-
};
175-
match run_result? {
176-
RunResult::Returned(rval) => {
177-
// Finished running, return UntypedReturnValue
178-
return Ok(Bounce(BounceInner::Done(rval)));
179-
}
180-
RunResult::Yielded(yval) => {
181-
// Check if the yield came from Vmctx::block_on:
182-
if yval.is::<YieldedFuture>() {
183-
let YieldedFuture(future) = *yval.downcast::<YieldedFuture>().unwrap();
184-
// Rehydrate the lifetime from `'static` to `'a`, which
185-
// is morally the same lifetime as was passed into
186-
// `Vmctx::block_on`.
187-
Ok(Bounce(BounceInner::More(
188-
future as BoxFuture<'a, ResumeVal>,
189-
)))
190-
} else {
191-
// Any other yielded value is not supported - die with an error.
192-
Err(Error::Unsupported(
193-
"cannot yield anything besides a future in Instance::run_async"
194-
.to_owned(),
195-
))
196-
}
197-
}
184+
let run_result = if self.is_yielded() {
185+
// A previous iteration of the loop stored the ResumeVal in
186+
// `resume_val`, send it back to the guest ctx and continue
187+
// running:
188+
self.resume_with_val_impl(
189+
resume_val
190+
.take()
191+
.expect("is_yielded implies resume_value is some"),
192+
true,
193+
runtime_bound,
194+
)
195+
} else if self.is_bound_expired() {
196+
self.resume_bounded(
197+
runtime_bound.expect("should have bound if guest had expired bound"),
198+
)
199+
} else {
200+
// This is the first iteration, call the entrypoint:
201+
self.run_func(func, args, true, runtime_bound)
202+
};
203+
match run_result? {
204+
InternalRunResult::Normal(RunResult::Returned(rval)) => {
205+
// Finished running, return UntypedReturnValue
206+
return Ok(rval);
198207
}
199-
})?;
200-
match bounce {
201-
Bounce(BounceInner::Done(rval)) => return Ok(rval),
202-
Bounce(BounceInner::More(fut)) => {
203-
// await on the computation. Store its result in
204-
// `resume_val`.
205-
resume_val = Some(fut.await);
208+
InternalRunResult::Normal(RunResult::Yielded(yval)) => {
209+
// Check if the yield came from Vmctx::block_on:
210+
if yval.is::<YieldedFuture>() {
211+
let YieldedFuture(future) = *yval.downcast::<YieldedFuture>().unwrap();
212+
// Rehydrate the lifetime from `'static` to `'a`, which
213+
// is morally the same lifetime as was passed into
214+
// `Vmctx::block_on`.
215+
let future = future as BoxFuture<'a, ResumeVal>;
216+
217+
// await on the computation. Store its result in
218+
// `resume_val`.
219+
resume_val = Some(future.await);
206220
// Now we want to `Instance::resume_with_val` and start
207221
// this cycle over.
222+
} else {
223+
// Any other yielded value is not supported - die with an error.
224+
return Err(Error::Unsupported(
225+
"cannot yield anything besides a future in Instance::run_async"
226+
.to_owned(),
227+
));
228+
}
229+
}
230+
InternalRunResult::BoundExpired => {
231+
// Await on a simple future that yields once then is ready.
232+
YieldNow::new().await
208233
}
209234
}
210235
}

0 commit comments

Comments
 (0)