Skip to content

Commit 2e716b6

Browse files
committed
Switch to having a separate handle table per resource type
1 parent b1d3d02 commit 2e716b6

File tree

3 files changed

+98
-62
lines changed

3 files changed

+98
-62
lines changed

design/mvp/CanonicalABI.md

Lines changed: 54 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -241,21 +241,20 @@ canonical definition is defined to execute inside. The `may_enter` and
241241
`may_leave` fields are used to enforce the [component invariants]: `may_leave`
242242
indicates whether the instance may call out to an import and the `may_enter`
243243
state indicates whether the instance may be called from the outside world
244-
through an export. The `HandleTable` is defined next.
244+
through an export.
245245
```python
246246
class ComponentInstance:
247247
may_leave: bool
248248
may_enter: bool
249-
handles: HandleTable
249+
handles: HandleTables
250250

251251
def __init__(self):
252252
self.may_leave = True
253253
self.may_enter = True
254-
self.handles = HandleTable()
254+
self.handles = HandleTables()
255255
```
256-
257-
The `HandleTable` class is defined in terms of a collection of supporting
258-
runtime bookkeeping classes that we'll go through first.
256+
`HandleTables` is defined in terms of a collection of supporting runtime
257+
bookkeeping classes that we'll go through first.
259258

260259
The `ResourceType` class represents a resource type that has been defined by
261260
the specific component instance pointed to by `impl` with a particular
@@ -275,7 +274,6 @@ handle refers to.
275274
@dataclass
276275
class Handle:
277276
rep: int
278-
rt: ResourceType
279277
lend_count: int
280278

281279
@dataclass
@@ -286,13 +284,10 @@ class OwnHandle(Handle):
286284
class BorrowHandle(Handle):
287285
scope: BorrowScope
288286
```
289-
The `rt` field points to a runtime value representing the static
290-
[`resourcetype`](Explainer.md#type-definitions) of this handle and is used by
291-
dynamic type checking below. Lastly, the `lend_count` field maintains a count
292-
of the outstanding handles that were lent from this handle (by calls to
293-
`borrow`-taking functions). This count is used below to dynamically enforce the
294-
invariant that a handle cannot be dropped while it has currently lent out a
295-
`borrow`.
287+
The `lend_count` field maintains a count of the outstanding handles that were
288+
lent from this handle (by calls to `borrow`-taking functions). This count is
289+
used below to dynamically enforce the invariant that a handle cannot be
290+
dropped while it has currently lent out a `borrow`.
296291

297292
The `BorrowScope` class represents the scope of a single runtime call of a
298293
component-level function during which zero or more handles are borrowed.
@@ -326,8 +321,9 @@ lent out a `BorrowHandle` and are to be restored when the call finishes and
326321
stored inline in the stack frame with a layout specialized to the function
327322
signature, thereby avoiding dynamic allocation of `lenders` in many cases.
328323

329-
Based on these supporting runtime data structures, we can define the
330-
`HandleTable` in pieces, starting with its fields and the `insert` method:
324+
`HandleTable` (singular) encapsulates a single mutable, growable array
325+
of handles that all share the same `ResourceType`. Defining `HandleTable` in
326+
chunks, we start with the fields and `insert` method:
331327
```python
332328
class HandleTable:
333329
array: [Optional[Handle]]
@@ -358,26 +354,18 @@ The `get` method is used by other `HandleTable` methods and canonical
358354
definitions below and uses dynamic guards to catch out-of-bounds and
359355
use-after-free:
360356
```python
361-
def get(self, i, rt):
357+
def get(self, i):
362358
trap_if(i >= len(self.array))
363359
trap_if(self.array[i] is None)
364-
trap_if(self.array[i].rt is not rt)
365360
return self.array[i]
366361
```
367-
Additionally, the `get` method takes the runtime resource type tag and checks
368-
a tag match before returning the handle with this new-valid resource type. This
369-
check is a non-structural, pointer-equality-based test used to enforce the
370-
[type-checking rules](Explainer.md) of resource types at runtime. Importantly,
371-
this check keeps type imports abstract, considering each type import to have a
372-
unique `rt` value distinct from every other type import even if the two imports
373-
happen to be instantiated with the same resource type at runtime.
374-
375-
Finally, the `remove` method is used to drop or transfer a handle out of the
376-
handle table. `remove` adds the removed handle to the `free` list for later
377-
recycling by `insert` (above).
362+
363+
The last method of `HandleTable`, `remove`, is used to drop or transfer a
364+
handle out of the handle table. `remove` adds the removed handle to the `free`
365+
list for later recycling by `insert` (above).
378366
```python
379367
def remove(self, i, t):
380-
h = self.get(i, t.rt)
368+
h = self.get(i)
381369
trap_if(h.lend_count != 0)
382370
match t:
383371
case Own(_):
@@ -398,6 +386,36 @@ previous owner. The bookkeeping performed by `remove` for borrowed handles
398386
records the fulfillment of the obligation of the borrower to drop the handle
399387
before the end of the call.
400388

389+
Finally, we can define `HandleTables` (plural) as simply a wrapper around
390+
a mutable mapping from `ResourceType` to `HandleTable`:
391+
```python
392+
class HandleTables:
393+
rt_to_table: MutableMapping[ResourceType, HandleTable]
394+
395+
def __init__(self):
396+
self.rt_to_table = dict()
397+
398+
def table(self, rt):
399+
if id(rt) not in self.rt_to_table:
400+
self.rt_to_table[id(rt)] = HandleTable()
401+
return self.rt_to_table[id(rt)]
402+
403+
def insert(self, h, rt):
404+
return self.table(rt).insert(h)
405+
def get(self, i, rt):
406+
return self.table(rt).get(i)
407+
def remove(self, i, t):
408+
return self.table(t.rt).remove(i, t)
409+
```
410+
While this Python code performs a dynamic hash-table lookup on each handle
411+
table access, as we'll see below, the `rt` parameter is always statically
412+
known such that a normal implementation can statically enumerate all
413+
`HandleTable` objects at compile time and then route the calls to `insert`,
414+
`get` and `remove` to the correct `HandleTable` at the callsite. The net
415+
result is that each component instance will contain one handle table per
416+
resource type used by the component, with each compiled adapter function
417+
accessing the correct handle table as-if it were a global variable.
418+
401419

402420
### Loading
403421

@@ -963,16 +981,16 @@ current component instance's `HandleTable`:
963981
```python
964982
def lower_own(cx, src, rt):
965983
assert(isinstance(src, OwnHandle))
966-
h = OwnHandle(src.rep, rt, 0)
967-
return cx.inst.handles.insert(h)
984+
h = OwnHandle(src.rep, 0)
985+
return cx.inst.handles.insert(h, rt)
968986

969987
def lower_borrow(cx, src, rt):
970988
assert(isinstance(src, Handle))
971989
if cx.inst is rt.impl:
972990
return src.rep
973991
cx.borrow_scope.add(src)
974-
h = BorrowHandle(src.rep, rt, 0, cx.borrow_scope)
975-
return cx.inst.handles.insert(h)
992+
h = BorrowHandle(src.rep, 0, cx.borrow_scope)
993+
return cx.inst.handles.insert(h, rt)
976994
```
977995
The special case in `lower_borrow` is an optimization, recognizing that, when
978996
a borrowed handle is passed to the component that implemented the resource
@@ -1551,8 +1569,8 @@ Calling `$f` invokes the following function, which creates a resource object
15511569
and inserts it into the current instance's handle table:
15521570
```python
15531571
def canon_resource_new(inst, rt, rep):
1554-
h = OwnHandle(rep, rt, 0)
1555-
return inst.handles.insert(h)
1572+
h = OwnHandle(rep, 0)
1573+
return inst.handles.insert(h, rt)
15561574
```
15571575

15581576
### `canon resource.drop`

design/mvp/canonical-abi/definitions.py

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -306,12 +306,12 @@ class CanonicalOptions:
306306
class ComponentInstance:
307307
may_leave: bool
308308
may_enter: bool
309-
handles: HandleTable
309+
handles: HandleTables
310310

311311
def __init__(self):
312312
self.may_leave = True
313313
self.may_enter = True
314-
self.handles = HandleTable()
314+
self.handles = HandleTables()
315315

316316
#
317317

@@ -325,7 +325,6 @@ class ResourceType(Type):
325325
@dataclass
326326
class Handle:
327327
rep: int
328-
rt: ResourceType
329328
lend_count: int
330329

331330
@dataclass
@@ -381,16 +380,15 @@ def insert(self, h):
381380

382381
#
383382

384-
def get(self, i, rt):
383+
def get(self, i):
385384
trap_if(i >= len(self.array))
386385
trap_if(self.array[i] is None)
387-
trap_if(self.array[i].rt is not rt)
388386
return self.array[i]
389387

390388
#
391389

392390
def remove(self, i, t):
393-
h = self.get(i, t.rt)
391+
h = self.get(i)
394392
trap_if(h.lend_count != 0)
395393
match t:
396394
case Own(_):
@@ -402,6 +400,26 @@ def remove(self, i, t):
402400
self.free.append(i)
403401
return h
404402

403+
#
404+
405+
class HandleTables:
406+
rt_to_table: MutableMapping[ResourceType, HandleTable]
407+
408+
def __init__(self):
409+
self.rt_to_table = dict()
410+
411+
def table(self, rt):
412+
if id(rt) not in self.rt_to_table:
413+
self.rt_to_table[id(rt)] = HandleTable()
414+
return self.rt_to_table[id(rt)]
415+
416+
def insert(self, h, rt):
417+
return self.table(rt).insert(h)
418+
def get(self, i, rt):
419+
return self.table(rt).get(i)
420+
def remove(self, i, t):
421+
return self.table(t.rt).remove(i, t)
422+
405423
### Loading
406424

407425
def load(cx, ptr, t):
@@ -839,16 +857,16 @@ def pack_flags_into_int(v, labels):
839857

840858
def lower_own(cx, src, rt):
841859
assert(isinstance(src, OwnHandle))
842-
h = OwnHandle(src.rep, rt, 0)
843-
return cx.inst.handles.insert(h)
860+
h = OwnHandle(src.rep, 0)
861+
return cx.inst.handles.insert(h, rt)
844862

845863
def lower_borrow(cx, src, rt):
846864
assert(isinstance(src, Handle))
847865
if cx.inst is rt.impl:
848866
return src.rep
849867
cx.borrow_scope.add(src)
850-
h = BorrowHandle(src.rep, rt, 0, cx.borrow_scope)
851-
return cx.inst.handles.insert(h)
868+
h = BorrowHandle(src.rep, 0, cx.borrow_scope)
869+
return cx.inst.handles.insert(h, rt)
852870

853871
### Flattening
854872

@@ -1202,8 +1220,8 @@ def canon_lower(opts, inst, callee, calling_import, ft, flat_args):
12021220
### `resource.new`
12031221

12041222
def canon_resource_new(inst, rt, rep):
1205-
h = OwnHandle(rep, rt, 0)
1206-
return inst.handles.insert(h)
1223+
h = OwnHandle(rep, 0)
1224+
return inst.handles.insert(h, rt)
12071225

12081226
### `resource.drop`
12091227

design/mvp/canonical-abi/run_tests.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,7 @@ def host_import(args):
396396
assert(len(args) == 2)
397397
assert(args[0].rep == 42)
398398
assert(args[1].rep == 44)
399-
return ([OwnHandle(45, rt, 0)], lambda:())
399+
return ([OwnHandle(45, 0)], lambda:())
400400

401401
def core_wasm(args):
402402
nonlocal dtor_value
@@ -419,36 +419,36 @@ def core_wasm(args):
419419
dtor_value = None
420420
canon_resource_drop(inst, Own(rt), 0)
421421
assert(dtor_value == 42)
422-
assert(len(inst.handles.array) == 4)
423-
assert(inst.handles.array[0] is None)
424-
assert(len(inst.handles.free) == 1)
422+
assert(len(inst.handles.table(rt).array) == 4)
423+
assert(inst.handles.table(rt).array[0] is None)
424+
assert(len(inst.handles.table(rt).free) == 1)
425425

426426
h = canon_resource_new(inst, rt, 46)
427427
assert(h == 0)
428-
assert(len(inst.handles.array) == 4)
429-
assert(inst.handles.array[0] is not None)
430-
assert(len(inst.handles.free) == 0)
428+
assert(len(inst.handles.table(rt).array) == 4)
429+
assert(inst.handles.table(rt).array[0] is not None)
430+
assert(len(inst.handles.table(rt).free) == 0)
431431

432432
dtor_value = None
433433
canon_resource_drop(inst, Borrow(rt), 2)
434434
assert(dtor_value is None)
435-
assert(len(inst.handles.array) == 4)
436-
assert(inst.handles.array[2] is None)
437-
assert(len(inst.handles.free) == 1)
435+
assert(len(inst.handles.table(rt).array) == 4)
436+
assert(inst.handles.table(rt).array[2] is None)
437+
assert(len(inst.handles.table(rt).free) == 1)
438438

439439
return [Value('i32', 0), Value('i32', 1), Value('i32', 3)]
440440

441441
ft = FuncType([Own(rt),Own(rt),Borrow(rt),Borrow(rt2)],[Own(rt),Own(rt),Own(rt)])
442-
args = [OwnHandle(42, rt, 0), OwnHandle(43, rt, 0), OwnHandle(44, rt, 0), OwnHandle(13, rt2, 0)]
442+
args = [OwnHandle(42, 0), OwnHandle(43, 0), OwnHandle(44, 0), OwnHandle(13, 0)]
443443
got,post_return = canon_lift(opts, inst, core_wasm, ft, args)
444444

445445
assert(len(got) == 3)
446446
assert(got[0].rep == 46)
447447
assert(got[1].rep == 43)
448448
assert(got[2].rep == 45)
449-
assert(len(inst.handles.array) == 4)
450-
assert(all(inst.handles.array[i] is None for i in range(3)))
451-
assert(len(inst.handles.free) == 4)
449+
assert(len(inst.handles.table(rt).array) == 4)
450+
assert(all(inst.handles.table(rt).array[i] is None for i in range(3)))
451+
assert(len(inst.handles.table(rt).free) == 4)
452452
definitions.MAX_FLAT_RESULTS = before
453453

454454
test_handles()

0 commit comments

Comments
 (0)