Skip to content

Commit

Permalink
feat: slab implementation for resource tables (#393)
Browse files Browse the repository at this point in the history
guybedford authored Mar 4, 2024
1 parent 59f1b55 commit 32cc612
Showing 3 changed files with 228 additions and 101 deletions.
52 changes: 37 additions & 15 deletions crates/js-component-bindgen/src/function_bindgen.rs
Original file line number Diff line number Diff line change
@@ -45,7 +45,8 @@ pub enum ResourceData {
///
/// For a given resource id {x}, the local variables are assumed:
/// - handleTable{x}
/// - handleCnt{x}
/// - captureTable{x} (only for imported tables)
/// - captureCnt{x} (only for imported tables)
///
/// For component-defined resources:
/// - finalizationRegistry{x}
@@ -1194,14 +1195,16 @@ impl Bindgen for FunctionBindgen<'_> {
} => {
let id = id.as_u32();
let symbol_dispose = self.intrinsic(Intrinsic::SymbolDispose);
let rsc_table_remove = self.intrinsic(Intrinsic::ResourceTableRemove);
if !imported {
let rep = format!("rep{}", self.tmp());
let symbol_resource_handle =
self.intrinsic(Intrinsic::SymbolResourceHandle);
let rsc_table_get = self.intrinsic(Intrinsic::ResourceTableGet);
uwrite!(
self.src,
"var {rsc} = new.target === {local_name} ? this : Object.create({local_name}.prototype);
var {rep} = handleTable{id}.get({handle}).rep;
var {rep} = {rsc_table_get}(handleTable{id}, {handle}).rep;
Object.defineProperty({rsc}, {symbol_resource_handle}, {{ writable: true, value: {rep} }});
",
);
@@ -1215,11 +1218,12 @@ impl Bindgen for FunctionBindgen<'_> {
self.src,
"finalizationRegistry{id}.register({rsc}, {handle}, {rsc});
Object.defineProperty({rsc}, {symbol_dispose}, {{ writable: true, value: function () {{{}}} }});
{rsc_table_remove}(handleTable{id}, {handle});
",
match dtor_name {
Some(dtor) => format!("
finalizationRegistry{id}.unregister({rsc});
handleTable{id}.delete({handle});
{rsc_table_remove}(handleTable{id}, {handle});
{rsc}[{symbol_dispose}] = {empty_func};
{rsc}[{symbol_resource_handle}] = null;
{dtor}({rep});
@@ -1236,12 +1240,15 @@ impl Bindgen for FunctionBindgen<'_> {
}
} else {
// imported handles lift as instance capture from a previous lowering
uwriteln!(self.src, "var {rsc} = handleTable{id}.get({handle}).rep;");
}

// an own lifting is a transfer to JS, so handle is implicitly dropped
if is_own {
uwriteln!(self.src, "handleTable{id}.delete({handle});");
let rsc_table_get = self.intrinsic(Intrinsic::ResourceTableGet);
uwriteln!(self.src, "var {rsc} = captureTable{id}.get({rsc_table_get}(handleTable{id}, {handle}).rep);");
// an own lifting is a transfer to JS, so handle is implicitly dropped
if is_own {
uwriteln!(
self.src,
"captureTable{id}.delete({rsc_table_remove}(handleTable{id}, {handle}).rep);"
);
}
}
}

@@ -1332,10 +1339,11 @@ impl Bindgen for FunctionBindgen<'_> {
// though, and their finalizers deregistered as well.
if is_own {
let empty_func = self.intrinsic(Intrinsic::EmptyFunc);
let rsc_table_create_own =
self.intrinsic(Intrinsic::ResourceTableCreateOwn);
uwriteln!(
self.src,
"var {handle} = handleCnt{id}++;
handleTable{id}.set({handle}, {{ rep: {rep}, own: true }});
"var {handle} = {rsc_table_create_own}(handleTable{id}, {rep});
finalizationRegistry{id}.unregister({op});
{op}[{symbol_dispose}] = {empty_func};
{op}[{symbol_resource_handle}] = null;"
@@ -1348,15 +1356,28 @@ impl Bindgen for FunctionBindgen<'_> {
}
} else {
// imported resources are always given a unique handle
// their assigned rep is deduped across usage though
uwriteln!(
self.src,
"if (!({op} instanceof {local_name})) {{
throw new Error('Resource error: Not a valid \"{class_name}\" resource.');
}}
var {handle} = handleCnt{id}++;
handleTable{id}.set({handle}, {{ rep: {op}, own: {is_own} }});",
captureTable{id}.set(++captureCnt{id}, {op});"
);
if is_own {
let rsc_table_create_own =
self.intrinsic(Intrinsic::ResourceTableCreateOwn);
uwriteln!(
self.src,
"var {handle} = {rsc_table_create_own}(handleTable{id}, captureCnt{id});",
);
} else {
let rsc_table_create_borrow =
self.intrinsic(Intrinsic::ResourceTableCreateBorrow);
uwriteln!(
self.src,
"var {handle} = {rsc_table_create_borrow}(handleTable{id}, captureCnt{id});",
);
}

// track lowered borrows to ensure they are dropped
// cur_resource_borrows can be reused because:
@@ -1365,8 +1386,9 @@ impl Bindgen for FunctionBindgen<'_> {
// - conversely, it is not possible to have a JS call that lowers a borrow handle argument
// and JS calls cannot return borrows for lowering
if !is_own && !self.valid_lifting_optimization {
let rsc_table_get = self.intrinsic(Intrinsic::ResourceTableGet);
self.cur_resource_borrows
.push(format!("handleTable{id}.get({handle})"));
.push(format!("{rsc_table_get}(handleTable{id}, {handle})"));
}
}
}
185 changes: 157 additions & 28 deletions crates/js-component-bindgen/src/intrinsics.rs
Original file line number Diff line number Diff line change
@@ -20,10 +20,17 @@ pub enum Intrinsic {
I64ToF64,
InstantiateCore,
IsLE,
ResourceTableFlag,
ResourceTableCreateBorrow,
ResourceTableCreateOwn,
ResourceTableGet,
ResourceTableTryGet,
ResourceTableRemove,
ResourceCallBorrows,
ResourceTransferBorrow,
ResourceTransferBorrowValidLifting,
ResourceTransferOwn,
ScopeId,
SymbolResourceHandle,
SymbolDispose,
ThrowInvalidBool,
@@ -191,21 +198,135 @@ pub fn render_intrinsics(

Intrinsic::ResourceCallBorrows => output.push_str("let resourceCallBorrows = [];"),

//
// # Resource table slab implementation
//
// Resource table slab implementation on top of a fixed "SMI" array in JS engines,
// a fixed contiguous array of u32s, for performance. We don't use a typed array because
// we need resizability without reserving a large buffer.
//
// The flag bit for all data values is 1 << 30. We avoid the use of the highest bit
// entirely to not trigger SMI deoptimization.
//
// Each entry consists of a pair of u32s, either a free list entry, or a data entry.
//
// ## Free List Entries:
//
// | index (x, u30) | ~unused~ |
// |------ 32 bits ------|------ 32 bits ------|
// | 01xxxxxxxxxxxxxxxxx | ################### |
//
// Free list entries use only the first value in the pair, with the high bit always set
// to indicate that the pair is part of the free list. The first pair of entries at
// indices 0 and 1 is the free list head, with the initial values of 1 << 30 and 0
// respectively. Removing the 1 << 30 flag gives 0, which indicates the end of the free
// list.
//
// ## Data Entries:
//
// | scope (x, u30) | own(o), rep(x, u30) |
// |------ 32 bits ------|------ 32 bits ------|
// | 00xxxxxxxxxxxxxxxxx | 0oxxxxxxxxxxxxxxxxx |
//
// Data entry pairs consist of a first u30 scope entry and a second rep entry. The field
// is only called the scope for interface shape consistency, but is actually used for the
// ref count for own handles and the scope id for borrow handles. The high bit is never
// set for this first entry to distinguish the pair from the free list. The second item
// in the pair is the rep for the resource, with the high bit in this entry indicating
// if it is an own handle.
//
// The free list numbering and the handle numbering are the same, indexing by pair, so to
// get from a handle or free list numbering to an index, we multiply by two.
//
// For example, to access a handle n, we read the pair of values n * 2 and n * 2 + 1 in
// the array to get the context and rep respectively. If the high bit is set on the
// context, we throw for an invalid handle. The rep value is masked out from the
// ownership high bit, also throwing for an invalid zero rep.
//
Intrinsic::ResourceTableFlag => output.push_str("
const T_FLAG = 1 << 30;
"),

Intrinsic::ResourceTableCreateBorrow => output.push_str("
function rscTableCreateBorrow (table, rep) {
if (rep === 0) throw new Error('Invalid rep');
const free = table[0] & ~T_FLAG;
if (free === 0) {
table.push(scopeId);
table.push(rep);
return (table.length >> 1) - 1;
}
table[0] = table[free];
table[free << 1] = scopeId;
table[(free << 1) + 1] = rep;
return free;
}
"),

Intrinsic::ResourceTableCreateOwn => output.push_str("
function rscTableCreateOwn (table, rep) {
if (rep === 0) throw new Error('Invalid rep');
const free = table[0] & ~T_FLAG;
if (free === 0) {
table.push(0);
table.push(rep | T_FLAG);
return (table.length >> 1) - 1;
}
table[0] = table[free << 1];
table[free << 1] = 0;
table[(free << 1) + 1] = rep | T_FLAG;
return free;
}
"),

Intrinsic::ResourceTableGet => output.push_str("
function rscTableGet (table, handle) {
const scope = table[handle << 1];
const val = table[(handle << 1) + 1];
const own = (val & T_FLAG) !== 0;
const rep = val & ~T_FLAG;
if (rep === 0 || (scope & T_FLAG) !== 0) throw new Error('Invalid handle');
return { rep, scope, own };
}
"),

Intrinsic::ResourceTableTryGet => output.push_str("
function rscTableTryGet (table, handle) {
const scope = table[handle << 1];
const val = table[(handle << 1) + 1];
const own = (val & T_FLAG) !== 0;
const rep = val & ~T_FLAG;
if (rep === 0 || (scope & T_FLAG) !== 0)
return;
return { rep, scope, own };
}
"),

Intrinsic::ResourceTableRemove => output.push_str("
function rscTableRemove (table, handle) {
const scope = table[handle << 1];
const val = table[(handle << 1) + 1];
const own = (val & T_FLAG) !== 0;
const rep = val & ~T_FLAG;
if (val === 0 || (scope & T_FLAG) !== 0) throw new Error('Invalid handle');
table[handle << 1] = table[0] | T_FLAG;
table[0] = handle | T_FLAG;
return { rep, scope, own };
}
"),

Intrinsic::ResourceTransferBorrow => {
let handle_tables = Intrinsic::HandleTables.name();
let resource_borrows = Intrinsic::ResourceCallBorrows.name();
let rsc_table_remove = Intrinsic::ResourceTableRemove.name();
let rsc_table_create_borrow = Intrinsic::ResourceTableCreateBorrow.name();
output.push_str(&format!("
function resourceTransferBorrow(handle, fromRid, toRid) {{
const {{ t: fromTable, l: fromLocal }} = {handle_tables}.get(fromRid);
let rep = handle;
if (!fromLocal) {{
({{ rep }} = fromTable.get(handle));
fromTable.delete(handle);
}}
const {{ t: toTable, h: createHandle, l: toLocal }} = {handle_tables}.get(toRid);
if (toLocal) return rep;
const newHandle = createHandle();
toTable.set(newHandle, {{ rep, own: false }});
const {{ t: fromTable, i: fromImport }} = {handle_tables}.get(fromRid);
const rep = fromImport ? {rsc_table_remove}(fromTable, handle) : handle;
const {{ t: toTable, i: toImport }} = {handle_tables}.get(toRid);
if (!toImport) return rep;
const newHandle = {rsc_table_create_borrow}(toTable, rep);
{resource_borrows}.push({{ rid: toRid, handle: newHandle }});
return newHandle;
}}
@@ -214,38 +335,37 @@ pub fn render_intrinsics(

Intrinsic::ResourceTransferBorrowValidLifting => {
let handle_tables = Intrinsic::HandleTables.name();
let rsc_table_remove = Intrinsic::ResourceTableRemove.name();
let rsc_table_create_borrow = Intrinsic::ResourceTableCreateBorrow.name();
output.push_str(&format!("
function resourceTransferBorrowValidLifting(handle, fromRid, toRid) {{
const {{ t: fromTable, l: fromLocal }} = {handle_tables}.get(fromRid);
let rep = handle;
if (!fromLocal) {{
({{ rep }} = fromTable.get(handle));
fromTable.delete(handle);
}}
const {{ t: toTable, h: createHandle, l: toLocal }} = {handle_tables}.get(toRid);
if (toLocal) return rep;
const newHandle = createHandle();
toTable.set(newHandle, {{ rep, own: false }});
return newHandle;
const {{ t: fromTable, i: fromImport }} = {handle_tables}.get(fromRid);
const rep = fromImport ? {rsc_table_remove}(fromTable, handle) : handle;
const {{ t: toTable, i: toImport }} = {handle_tables}.get(toRid);
if (!toImport) return rep;
return {rsc_table_create_borrow}(toTable, rep);
}}
"));
},

Intrinsic::ResourceTransferOwn => {
let handle_tables = Intrinsic::HandleTables.name();
let rsc_table_remove = Intrinsic::ResourceTableRemove.name();
let rsc_table_create_own = Intrinsic::ResourceTableCreateOwn.name();
output.push_str(&format!("
function resourceTransferOwn(handle, fromRid, toRid) {{
const {{ t: fromTable }} = {handle_tables}.get(fromRid);
const entry = fromTable.get(handle);
fromTable.delete(handle);
const {{ t: toTable, h }} = {handle_tables}.get(toRid);
const newHandle = h();
toTable.set(newHandle, entry);
return newHandle;
const {{ rep }} = {rsc_table_remove}(fromTable, handle);
const {{ t: toTable }} = {handle_tables}.get(toRid);
return {rsc_table_create_own}(toTable, rep);
}}
"));
},

Intrinsic::ScopeId => output.push_str("
let scopeId = 0;
"),

Intrinsic::SymbolResourceHandle => output.push_str("
const resourceHandleSymbol = Symbol('resource');
"),
@@ -262,7 +382,7 @@ pub fn render_intrinsics(

Intrinsic::ThrowUninitialized => output.push_str("
function throwUninitialized() {
throw new TypeError('Wasm uninitialized use `await $init` first');
throw new Error('Wasm uninitialized use `await $init` first');
}
"),

@@ -442,6 +562,8 @@ impl Intrinsic {
"dv",
"emptyFunc",
"Error",
"E_OWN",
"E_FREE",
"f32ToI32",
"f64ToI64",
"fetch",
@@ -503,9 +625,16 @@ impl Intrinsic {
Intrinsic::InstantiateCore => "instantiateCore",
Intrinsic::IsLE => "isLE",
Intrinsic::ResourceCallBorrows => "resourceCallBorrows",
Intrinsic::ResourceTableFlag => "T_FLAG",
Intrinsic::ResourceTableCreateBorrow => "rscTableCreateBorrow",
Intrinsic::ResourceTableCreateOwn => "rscTableCreateOwn",
Intrinsic::ResourceTableGet => "rscTableGet",
Intrinsic::ResourceTableTryGet => "rscTableTryGet",
Intrinsic::ResourceTableRemove => "rscTableRemove",
Intrinsic::ResourceTransferBorrow => "resourceTransferBorrow",
Intrinsic::ResourceTransferBorrowValidLifting => "resourceTransferBorrowValidLifting",
Intrinsic::ResourceTransferOwn => "resourceTransferOwn",
Intrinsic::ScopeId => "scopeId",
Intrinsic::SymbolResourceHandle => "resourceHandleSymbol",
Intrinsic::SymbolDispose => "symbolDispose",
Intrinsic::ThrowInvalidBool => "throwInvalidBool",
92 changes: 34 additions & 58 deletions crates/js-component-bindgen/src/transpile_bindgen.rs
Original file line number Diff line number Diff line change
@@ -585,25 +585,27 @@ impl<'a> Instantiator<'a, '_> {
};

let handle_tables = self.gen.intrinsic(Intrinsic::HandleTables);
let rsc_table_flag = self.gen.intrinsic(Intrinsic::ResourceTableFlag);
let rsc_table_remove = self.gen.intrinsic(Intrinsic::ResourceTableRemove);

if is_imported {
// imported resouces have both a rep table and a rep assignment
// for captured resource classes to assign them a rep numbering
uwriteln!(
self.src.js,
"const handleTable{rid} = new Map();
let handleCnt{rid} = 1;
{handle_tables}.set({rid}, {{ t: handleTable{rid}, h: () => ++handleCnt{rid}, l: false }});",
"const handleTable{rid} = [{rsc_table_flag}, 0];
const captureTable{rid} = new Map();
let captureCnt{rid} = 0;
{handle_tables}.set({rid}, {{ t: handleTable{rid}, i: captureTable{rid}.get.bind(captureTable{rid}) }});",
);
} else {
uwriteln!(
self.src.js,
"const handleTable{rid} = new Map();
let handleCnt{rid} = 1;
"const handleTable{rid} = [{rsc_table_flag}, 0];
const finalizationRegistry{rid} = new FinalizationRegistry((handle) => {{
const handleEntry = handleTable{rid}.get(handle);
const {{ rep }} = handleEntry;
handleTable{rid}.delete(handle);{maybe_dtor}
const {{ rep }} = {rsc_table_remove}(handleTable{rid}, handle);{maybe_dtor}
}});
{handle_tables}.set({rid}, {{ t: handleTable{rid}, h: () => ++handleCnt{rid}, l: true }});
{handle_tables}.set({rid}, {{ t: handleTable{rid}, i: null }});
",
);
}
@@ -700,29 +702,19 @@ impl<'a> Instantiator<'a, '_> {
Trampoline::ResourceNew(resource) => {
self.ensure_resource_table(*resource);
let rid = resource.as_u32();
uwrite!(
let rsc_table_create_own = self.gen.intrinsic(Intrinsic::ResourceTableCreateOwn);
uwriteln!(
self.src.js,
"function trampoline{i}(rep) {{
const handle = handleCnt{rid}++;
handleTable{rid}.set(handle, {{ rep, own: true }});
return handle;
}}
"
"const trampoline{i} = {rsc_table_create_own}.bind(null, handleTable{rid});"
);
}
Trampoline::ResourceRep(resource) => {
self.ensure_resource_table(*resource);
let rid = resource.as_u32();
uwrite!(
let rsc_table_get = self.gen.intrinsic(Intrinsic::ResourceTableGet);
uwriteln!(
self.src.js,
"function trampoline{i}(handle) {{
const handleEntry = handleTable{rid}.get(handle);
if (!handleEntry) {{
throw new Error(`Resource error: Invalid handle ${{handle}}`);
}}
return handleEntry.rep;
}}
"
"const trampoline{i} = {rsc_table_get}.bind(null, handleTable{rid});"
);
}
Trampoline::ResourceDrop(resource) => {
@@ -743,13 +735,7 @@ impl<'a> Instantiator<'a, '_> {
.unwrap();

if let Some(dtor) = &resource_def.dtor {
format!(
"if (handleEntry.own) {{
{}(handleEntry.rep);
}}
",
self.core_def(dtor)
)
format!("{}(handleEntry.rep);", self.core_def(dtor))
} else {
"".into()
}
@@ -758,20 +744,18 @@ impl<'a> Instantiator<'a, '_> {
// resources when the resource is dropped
let symbol_dispose = self.gen.intrinsic(Intrinsic::SymbolDispose);
format!(
"if (handleEntry.own && handleEntry.rep[{symbol_dispose}]) {{
handleEntry.rep[{symbol_dispose}]();
}}"
"const rsc = captureTable{rid}.get(handleEntry.rep);
if (rsc[{symbol_dispose}]) rsc[{symbol_dispose}]();
captureTable{rid}.delete(handleEntry.rep);"
)
};

let rsc_table_remove = self.gen.intrinsic(Intrinsic::ResourceTableRemove);
uwrite!(
self.src.js,
"function trampoline{i}(handle) {{
const handleEntry = handleTable{rid}.get(handle);
if (!handleEntry) {{
throw new Error(`Resource error: Invalid handle ${{handle}}`);
}}
handleTable{rid}.delete(handle);
const handleEntry = {rsc_table_remove}(handleTable{rid}, handle);
if (!handleEntry.own) throw new Error('Unexpected borrow handle');
{dtor}
}}
",
@@ -792,38 +776,30 @@ impl<'a> Instantiator<'a, '_> {
uwriteln!(self.src.js, "const trampoline{i} = {resource_transfer};");
}
Trampoline::ResourceEnterCall => {
// Resource enter call does not do anything in Jco as all the logic is handled
// by exit call based on the invariant that resourceCallBorrows should always
// be an empty array here.
let empty_func = self.gen.intrinsic(Intrinsic::EmptyFunc);
let scope_id = self.gen.intrinsic(Intrinsic::ScopeId);
uwrite!(
self.src.js,
"const trampoline{i} = {empty_func};
"function trampoline{i}() {{
{scope_id}++;
}}
",
);
}
Trampoline::ResourceExitCall => {
// Resource exit call is responsible for handling dynamic borrow drop checks
// for transferred resources (disabled for valid_lifting_optimization).
if self.gen.opts.valid_lifting_optimization {
let empty_func = self.gen.intrinsic(Intrinsic::EmptyFunc);
uwrite!(
self.src.js,
"const trampoline{i} = {empty_func};
",
);
return;
}
let scope_id = self.gen.intrinsic(Intrinsic::ScopeId);
let resource_borrows = self.gen.intrinsic(Intrinsic::ResourceCallBorrows);
let handle_tables = self.gen.intrinsic(Intrinsic::HandleTables);
let rsc_table_try_get = self.gen.intrinsic(Intrinsic::ResourceTableCreateOwn);
uwrite!(
self.src.js,
"function trampoline{i}() {{
for (const {{ rid, handle }} of {resource_borrows}) {{
if ({handle_tables}.get(rid).has(handle))
for (const {{ rid, handle, rep, scope }} of {resource_borrows}) {{
const entry = {rsc_table_try_get}({handle_tables}.get(rid), handle);
if (entry && entry.rep === rep && entry.scope === scope)
throw new Error('borrow was not dropped for resource transfer call');
}}
{resource_borrows} = [];
{scope_id}--;
}}
",
);

0 comments on commit 32cc612

Please sign in to comment.