Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
205 changes: 190 additions & 15 deletions crates/js-component-bindgen/src/function_bindgen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ use std::mem;

use heck::{ToLowerCamelCase, ToUpperCamelCase};
use wasmtime_environ::component::{
CanonicalOptions, InterfaceType, ResourceIndex, TypeComponentLocalErrorContextTableIndex,
TypeFutureTableIndex, TypeResourceTableIndex, TypeStreamTableIndex,
CanonicalOptions, CanonicalOptionsDataModel, InterfaceType, LinearMemoryOptions, ResourceIndex,
TypeComponentLocalErrorContextTableIndex, TypeFutureTableIndex, TypeResourceTableIndex,
TypeStreamTableIndex,
};
use wit_bindgen_core::abi::{Bindgen, Bitcast, Instruction};
use wit_component::StringEncoding;
Expand Down Expand Up @@ -396,12 +397,16 @@ impl FunctionBindgen<'_> {
));
let component_instance_idx = self.canon_opts.instance.as_u32();

// If we're within an async function, wait for all top level previous tasks to finish before running
// If we're within a true async function, or one that has simply been lowered as async,
// wait for all top level previous tasks to finish before running
// to ensure that guests do not try to run two tasks at the same time.
if is_async && self.requires_async_porcelain {
if is_async || self.requires_async_porcelain {
uwriteln!(
self.src,
r#"
// TODO(fix): this logic should be in the async task.enter() and be backpressure driven
// (note, we may need specific handlers for backpressure changes)
//
// All other tasks must finish before we can start this one
const taskMetas = {global_task_map}.get({component_instance_idx});
if (taskMetas) {{
Expand Down Expand Up @@ -2596,19 +2601,172 @@ impl Bindgen for FunctionBindgen<'_> {
results.push(item.clone());
}

Instruction::FutureLower { .. } => {
// TODO: convert this return of the lifted Future:
//
// ```
// return BigInt(writeEndWaitableIdx) << 32n | BigInt(readEndWaitableIdx);
// ```
//
// Into a component-local Future instance
//
Instruction::FutureLower { ty, .. } => {
let debug_log_fn = self.intrinsic(Intrinsic::DebugLog);
let get_or_create_async_state_fn = self.intrinsic(Intrinsic::Component(
ComponentIntrinsic::GetOrCreateAsyncState,
));
let gen_future_host_inject_fn = self.intrinsic(Intrinsic::AsyncFuture(
AsyncFutureIntrinsic::GenFutureHostInjectFn,
));
let is_future_lowerable_object_fn = self.intrinsic(Intrinsic::AsyncFuture(
AsyncFutureIntrinsic::IsFutureLowerableObject,
));

let component_idx = self.canon_opts.instance.as_u32();

let future_arg = operands
.first()
.expect("unexpectedly missing ErrorContextLower arg");
results.push(future_arg.clone());

// Build the lowering function for the type produced by the future
let type_id = &crate::dealias(self.resolve, *ty);
let ResourceTable {
imported: true,
data:
ResourceData::Guest {
extra:
Some(ResourceExtraData::Future {
table_idx: future_table_idx_ty,
elem_ty,
}),
..
},
} = self
.resource_map
.get(type_id)
.expect("missing resource mapping for future lower")
else {
unreachable!("invalid resource table observed during future lower");
};
let future_table_idx = future_table_idx_ty.as_u32();

// Generate payload metadata ('elemMeta')
let (
payload_type_name_js,
lift_fn_js,
lower_fn_js,
payload_is_none,
payload_is_numeric,
payload_is_borrow,
payload_is_async_value,
payload_size32_js,
payload_align32_js,
payload_flat_count_js,
) = match elem_ty {
Some(PayloadTypeMetadata {
ty: _,
iface_ty,
lift_js_expr,
lower_js_expr,
size32,
align32,
flat_count,
}) => (
format!("'{iface_ty:?}'"),
lift_js_expr.as_str(),
lower_js_expr.as_str(),
"false",
format!(
"{}",
matches!(
iface_ty,
InterfaceType::U8
| InterfaceType::U16
| InterfaceType::U32
| InterfaceType::U64
| InterfaceType::S8
| InterfaceType::S16
| InterfaceType::S32
| InterfaceType::S64
| InterfaceType::Float32
| InterfaceType::Float64
)
),
format!("{}", matches!(iface_ty, InterfaceType::Borrow(_))),
format!(
"{}",
matches!(
iface_ty,
InterfaceType::Stream(_) | InterfaceType::Future(_)
)
),
size32.to_string(),
align32.to_string(),
flat_count.unwrap_or(0).to_string(),
),
None => (
"null".into(),
"() => {{ throw new Error('no lift fn'); }}",
"() => {{ throw new Error('no lower fn'); }}",
"true",
"false".into(),
"false".into(),
"false".into(),
"null".into(),
"null".into(),
"0".into(),
),
};

// Retrieve the realloc fn if present, in case lowering fns need to allocate
//
// The realloc fn is saved on the element metadata which is passed through to
// stream end and underlying buffer
let get_realloc_fn_js = match self.canon_opts.data_model {
CanonicalOptionsDataModel::LinearMemory(LinearMemoryOptions {
realloc: Some(realloc_idx),
..
}) => format!("() => realloc{}", realloc_idx.as_u32()),
_ => "undefined".into(),
};

let tmp = self.tmp();
let lowered_future_waitable_idx = format!("futureWaitableIdx{tmp}");
uwriteln!(
self.src,
r#"
if (!{is_future_lowerable_object_fn}({future_arg})) {{
{debug_log_fn}('[Instruction::FutureLower] object is not a Promise/Thenable', {{ {future_arg} }});
throw new Error('unrecognized future object (not Promise/Thenable)');
}}

const cstate{tmp} = {get_or_create_async_state_fn}({component_idx});
if (!cstate{tmp}) {{ throw new Error(`missing component state for component [{component_idx}]`); }}

// TODO(feat): facilitate non utf8 string encoding for lowered futures
const stringEncoding = 'utf8';

const {{ writeEnd: hostWriteEnd{tmp}, readEnd: readEnd{tmp} }} = cstate{tmp}.createFuture({{
tableIdx: {future_table_idx},
elemMeta: {{
liftFn: {lift_fn_js},
lowerFn: {lower_fn_js},
payloadTypeName: {payload_type_name_js},
isNone: {payload_is_none},
isNumeric: {payload_is_numeric},
isBorrowed: {payload_is_borrow},
isAsyncValue: {payload_is_async_value},
flatCount: {payload_flat_count_js},
align32: {payload_align32_js},
size32: {payload_size32_js},
stringEncoding,
getReallocFn: {get_realloc_fn_js},
}},
}});

const hostInjectFn = {gen_future_host_inject_fn}({{
promise: {future_arg},
stringEncoding,
hostWriteEnd: hostWriteEnd{tmp},
}});
readEnd{tmp}.setHostInjectFn(hostInjectFn);

const {lowered_future_waitable_idx} = readEnd{tmp}.waitableIdx();
"#
);

results.push(lowered_future_waitable_idx);
}

Instruction::FutureLift { payload, ty } => {
Expand Down Expand Up @@ -2713,9 +2871,12 @@ impl Bindgen for FunctionBindgen<'_> {
ComponentIntrinsic::GetOrCreateAsyncState,
));
let gen_host_inject_fn = self.intrinsic(Intrinsic::AsyncStream(
AsyncStreamIntrinsic::GenHostInjectFn,
AsyncStreamIntrinsic::GenStreamHostInjectFn,
));

// TODO(???): A component could end up receiving a stream that it outputted,
// and the below would fail (imported: false)?

// Build the lowering function for the type produced by the stream
let type_id = &crate::dealias(self.resolve, *ty);
let ResourceTable {
Expand Down Expand Up @@ -2807,6 +2968,18 @@ impl Bindgen for FunctionBindgen<'_> {
),
};

// Retrieve the realloc fn if present, in case lowering fns need to allocate
//
// The realloc fn is saved on the element metadata which is passed through to
// stream end and underlying buffer
let get_realloc_fn_js = match self.canon_opts.data_model {
CanonicalOptionsDataModel::LinearMemory(LinearMemoryOptions {
realloc: Some(realloc_idx),
..
}) => format!("() => realloc{}", realloc_idx.as_u32()),
_ => "undefined".into(),
};

let tmp = self.tmp();
let lowered_stream_waitable_idx = format!("streamWaitableIdx{tmp}");
uwriteln!(
Expand Down Expand Up @@ -2837,6 +3010,7 @@ impl Bindgen for FunctionBindgen<'_> {
size32: {payload_size32_js},
// TODO(feat): facilitate non utf8 string encoding for lowered streams
stringEncoding: 'utf8',
geReallocFn: {get_realloc_fn_js},
}},
}});

Expand All @@ -2863,6 +3037,7 @@ impl Bindgen for FunctionBindgen<'_> {
const {lowered_stream_waitable_idx} = readEnd{tmp}.waitableIdx();
"#
);

results.push(lowered_stream_waitable_idx);
}

Expand Down
20 changes: 7 additions & 13 deletions crates/js-component-bindgen/src/intrinsics/lift.rs
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ impl LiftIntrinsic {
let val;

if (ctx.useDirectParams) {{
if (params.length === 0) {{ throw new Error('expected at least a single i32 argument'); }}
if (ctx.params.length === 0) {{ throw new Error('expected at least a single i32 argument'); }}
val = ctx.params[0];
ctx.params = ctx.params.slice(1);
return [val, ctx];
Expand Down Expand Up @@ -734,11 +734,6 @@ impl LiftIntrinsic {
return function {lift_flat_record_fn}Inner(ctx) {{
{debug_log_fn}('[{lift_flat_record_fn}()] args', {{ ctx }});

if (ctx.useDirectParams) {{
ctx.storagePtr = ctx.params[0];
ctx.params = ctx.params.slice(1);
}}

const res = {{}};
for (const [key, liftFn, _size32, _align32] of keysAndLiftFns) {{
let [val, newCtx] = liftFn(ctx);
Expand Down Expand Up @@ -1233,28 +1228,27 @@ impl LiftIntrinsic {
output.push_str(&format!(r#"
function {lift_flat_error_fn}(errCtxTableIdx, ctx) {{
{debug_log_fn}('[{lift_flat_error_fn}()] ctx', ctx);
const {{ useDirectParams, params, componentIdx }} = ctx;

let val;
let table;
if (useDirectParams) {{
if (params.length === 0) {{ throw new Error('expected at least one single i32 argument'); }}
if (ctx.useDirectParams) {{
if (ctx.params.length === 0) {{ throw new Error('expected at least one single i32 argument'); }}
val = ctx.params[0];
ctx.params = ctx.params.slice(1);
table = {get_err_ctx_local_table_fn}(componentIdx, errCtxTableIdx);
table = {get_err_ctx_local_table_fn}(ctx.componentIdx, errCtxTableIdx);
}} else {{
throw new Error('indirect flat lift for error-contexts not yet implemented!');
}}

let handle = table.get(val);
if (handle === undefined) {{
throw new Error(`missing error ctx (handle [${{val}}], component [${{componentIdx}}], error context table [${{errCtxTableIdx}}])`);
throw new Error(`missing error ctx (handle [${{val}}], component [${{ctx.componentIdx}}], error context table [${{errCtxTableIdx}}])`);
}}

const cstate = {get_or_create_async_state_fn}(componentIdx);
const cstate = {get_or_create_async_state_fn}(ctx.componentIdx);
const errCtx = cstate.handles.get(handle);
if (!errCtx || errCtx.globalRep === undefined || errCtx.refCount === undefined) {{
throw new Error(`malformed error context (handle [${{handle}}], component [${{componentIdx}}])`);
throw new Error(`malformed error context (handle [${{handle}}], component [${{ctx.componentIdx}}])`);
}}

errCtx.refCount -= 1;
Expand Down
4 changes: 2 additions & 2 deletions crates/js-component-bindgen/src/intrinsics/lower.rs
Original file line number Diff line number Diff line change
Expand Up @@ -498,7 +498,7 @@ impl LowerIntrinsic {
function {lower_flat_f64_fn}(ctx) {{
{debug_log_fn}('[{lower_flat_f64_fn}()] args', {{ ctx }});

if (vals.length !== 1) {{ throw new Error('unexpected number of vals'); }}
if (ctx.vals.length !== 1) {{ throw new Error('unexpected number of vals'); }}

const rem = ctx.storagePtr % 8;
if (rem !== 0) {{ ctx.storagePtr += (8 - rem); }}
Expand Down Expand Up @@ -994,7 +994,7 @@ impl LowerIntrinsic {
Intrinsic::AsyncStream(AsyncStreamIntrinsic::GenReadFnFromLowerableStream)
.name();
let gen_host_inject_fn =
Intrinsic::AsyncStream(AsyncStreamIntrinsic::GenHostInjectFn).name();
Intrinsic::AsyncStream(AsyncStreamIntrinsic::GenStreamHostInjectFn).name();
let lower_u32_fn = Self::LowerFlatU32.name();

output.push_str(&format!(
Expand Down
29 changes: 27 additions & 2 deletions crates/js-component-bindgen/src/intrinsics/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -614,7 +614,8 @@ impl Intrinsic {
storagePtr: startPtr,
componentIdx: this.#componentIdx,
stringEncoding: this.#elemMeta.stringEncoding,
realloc: this.#elemMeta.reallocFn,
realloc: this.#elemMeta.getReallocFn?.(),
getReallocFn: this.#elemMeta.getReallocFn,
}}
for (const v of values) {{
lowerCtx.vals = [v];
Expand Down Expand Up @@ -1367,7 +1368,22 @@ pub fn render_intrinsics(args: RenderIntrinsicsArgs) -> Source {
&Intrinsic::SymbolResourceRep,
&Intrinsic::Component(ComponentIntrinsic::GetOrCreateAsyncState),
&Intrinsic::AsyncStream(AsyncStreamIntrinsic::GenReadFnFromLowerableStream),
&Intrinsic::AsyncStream(AsyncStreamIntrinsic::GenHostInjectFn),
&Intrinsic::AsyncStream(AsyncStreamIntrinsic::GenStreamHostInjectFn),
&Intrinsic::Lower(LowerIntrinsic::LowerFlatU32),
])
}

if args
.intrinsics
.contains(&Intrinsic::Lower(LowerIntrinsic::LowerFlatFuture))
{
args.intrinsics.extend([
&Intrinsic::AsyncFuture(AsyncFutureIntrinsic::GlobalFutureMap),
&Intrinsic::AsyncFuture(AsyncFutureIntrinsic::InternalFutureClass),
&Intrinsic::AsyncFuture(AsyncFutureIntrinsic::IsFutureLowerableObject),
&Intrinsic::SymbolResourceRep,
&Intrinsic::Component(ComponentIntrinsic::GetOrCreateAsyncState),
&Intrinsic::AsyncFuture(AsyncFutureIntrinsic::GenFutureHostInjectFn),
&Intrinsic::Lower(LowerIntrinsic::LowerFlatU32),
])
}
Expand Down Expand Up @@ -1501,6 +1517,15 @@ pub fn render_intrinsics(args: RenderIntrinsicsArgs) -> Source {
]);
}

if args.intrinsics.contains(&Intrinsic::AsyncFuture(
AsyncFutureIntrinsic::FutureNewFromLift,
)) {
args.intrinsics.extend([
&Intrinsic::AsyncFuture(AsyncFutureIntrinsic::GlobalFutureMap),
&Intrinsic::AsyncFuture(AsyncFutureIntrinsic::HostFutureClass),
]);
}

if args.intrinsics.contains(&Intrinsic::AsyncFuture(
AsyncFutureIntrinsic::FutureWritableEndClass,
)) || args.intrinsics.contains(&Intrinsic::AsyncFuture(
Expand Down
Loading
Loading