Skip to content

Commit ba5ce25

Browse files
committed
Refactor handle logic, no change in behavior
1 parent 2e716b6 commit ba5ce25

File tree

3 files changed

+168
-150
lines changed

3 files changed

+168
-150
lines changed

design/mvp/CanonicalABI.md

Lines changed: 87 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -218,13 +218,25 @@ definitions via the `cx` parameter:
218218
class Context:
219219
opts: CanonicalOptions
220220
inst: ComponentInstance
221-
borrow_scope: BorrowScope
221+
lenders: [Handle]
222+
borrow_count: int
222223

223224
def __init__(self, opts, inst):
224225
self.opts = opts
225226
self.inst = inst
226-
self.borrow_scope = BorrowScope()
227+
self.lenders = []
228+
self.borrow_count = 0
227229
```
230+
One `Context` is created for each call for both the caller and callee. Thus, a
231+
cross-component call will create 2 `Context` objects for the call, while a
232+
component-to-host or host-to-component call will create a single `Context` for
233+
the wasm caller or callee, resp.
234+
235+
The `lenders` and `borrow_count` fields will be used below for dynamically
236+
enforcing the rules of borrow handles. The one thing to mention here is that
237+
the `lenders` list usually has a fixed size (in all cases except when a
238+
function signature has `borrow`s in `list`s) and thus can be stored inline in
239+
the native stack frame.
228240

229241
The `opts` field represents the [`canonopt`] values supplied to
230242
currently-executing `canon lift` or `canon lower`:
@@ -282,48 +294,22 @@ class OwnHandle(Handle):
282294

283295
@dataclass
284296
class BorrowHandle(Handle):
285-
scope: BorrowScope
297+
cx: Optional[Context]
286298
```
287299
The `lend_count` field maintains a count of the outstanding handles that were
288300
lent from this handle (by calls to `borrow`-taking functions). This count is
289301
used below to dynamically enforce the invariant that a handle cannot be
290302
dropped while it has currently lent out a `borrow`.
291303

292-
The `BorrowScope` class represents the scope of a single runtime call of a
293-
component-level function during which zero or more handles are borrowed.
294-
```python
295-
class BorrowScope:
296-
borrow_count: int
297-
lenders: [Handle]
298-
299-
def __init__(self):
300-
self.borrow_count = 0
301-
self.lenders = []
302-
303-
def add(self, src):
304-
src.lend_count += 1
305-
self.lenders.append(src)
306-
self.borrow_count += 1
307-
308-
def remove(self):
309-
self.borrow_count -= 1
310-
311-
def exit(self):
312-
trap_if(self.borrow_count != 0)
313-
for h in self.lenders:
314-
h.lend_count -= 1
315-
```
316-
The `borrow_count` field tracks the number of outstanding `BorrowHandle`s that
317-
were created when lowering the parameters of the call that have not yet been
318-
dropped. The `lenders` field maintains a list of source `Handle`s that have
319-
lent out a `BorrowHandle` and are to be restored when the call finishes and
320-
`exit` is called. In an optimizing implementation, a `BorrowScope` can be
321-
stored inline in the stack frame with a layout specialized to the function
322-
signature, thereby avoiding dynamic allocation of `lenders` in many cases.
304+
The `BorrowHandle` class additionally stores the `Context` of the call that
305+
created this borrow for the purposes of `borrow_count` bookkeeping below.
306+
Until async is added to the Component Model, because of the non-reentrancy
307+
of components, there is at most one callee `Context` alive for a given
308+
component and thus the `cx` field of `BorrowHandle` can be optimized away.
323309

324310
`HandleTable` (singular) encapsulates a single mutable, growable array
325311
of handles that all share the same `ResourceType`. Defining `HandleTable` in
326-
chunks, we start with the fields and `insert` method:
312+
chunks, we start with the fields and `add` method:
327313
```python
328314
class HandleTable:
329315
array: [Optional[Handle]]
@@ -333,20 +319,23 @@ class HandleTable:
333319
self.array = []
334320
self.free = []
335321

336-
def insert(self, h):
322+
def add(self, h, t):
337323
if self.free:
338324
i = self.free.pop()
339325
assert(self.array[i] is None)
340326
self.array[i] = h
341327
else:
342328
i = len(self.array)
343329
self.array.append(h)
330+
match t:
331+
case Borrow():
332+
h.cx.borrow_count += 1
344333
return i
345334
```
346335
The `HandleTable` class maintains a dense array of handles that can contain
347336
holes created by the `remove` method (defined below). These holes are kept in a
348337
separate Python list here, but an optimizing implementation could instead store
349-
the free list in the free elements of `array`. When inserting a new handle,
338+
the free list in the free elements of `array`. When adding a new handle,
350339
`HandleTable` first consults the `free` list, which is popped LIFO to better
351340
detect use-after-free bugs in the guest code.
352341

@@ -362,17 +351,20 @@ use-after-free:
362351

363352
The last method of `HandleTable`, `remove`, is used to drop or transfer a
364353
handle out of the handle table. `remove` adds the removed handle to the `free`
365-
list for later recycling by `insert` (above).
354+
list for later recycling by `add` (above).
366355
```python
367-
def remove(self, i, t):
356+
def remove_or_drop(self, i, t, drop):
368357
h = self.get(i)
369358
trap_if(h.lend_count != 0)
370359
match t:
371-
case Own(_):
360+
case Own():
372361
trap_if(not isinstance(h, OwnHandle))
373-
case Borrow(_):
362+
if drop and t.rt.dtor:
363+
trap_if(not t.rt.impl.may_enter)
364+
t.rt.dtor(h.rep)
365+
case Borrow():
374366
trap_if(not isinstance(h, BorrowHandle))
375-
h.scope.remove()
367+
h.cx.borrow_count -= 1
376368
self.array[i] = None
377369
self.free.append(i)
378370
return h
@@ -400,17 +392,19 @@ class HandleTables:
400392
self.rt_to_table[id(rt)] = HandleTable()
401393
return self.rt_to_table[id(rt)]
402394

403-
def insert(self, h, rt):
404-
return self.table(rt).insert(h)
395+
def add(self, h, t):
396+
return self.table(t.rt).add(h, t)
405397
def get(self, i, rt):
406398
return self.table(rt).get(i)
407399
def remove(self, i, t):
408-
return self.table(t.rt).remove(i, t)
400+
return self.table(t.rt).remove_or_drop(i, t, drop = False)
401+
def drop(self, i, t):
402+
self.table(t.rt).remove_or_drop(i, t, drop = True)
409403
```
410404
While this Python code performs a dynamic hash-table lookup on each handle
411405
table access, as we'll see below, the `rt` parameter is always statically
412406
known such that a normal implementation can statically enumerate all
413-
`HandleTable` objects at compile time and then route the calls to `insert`,
407+
`HandleTable` objects at compile time and then route the calls to `add`,
414408
`get` and `remove` to the correct `HandleTable` at the callsite. The net
415409
result is that each component instance will contain one handle table per
416410
resource type used by the component, with each compiled adapter function
@@ -445,8 +439,8 @@ def load(cx, ptr, t):
445439
case Record(fields) : return load_record(cx, ptr, fields)
446440
case Variant(cases) : return load_variant(cx, ptr, cases)
447441
case Flags(labels) : return load_flags(cx, ptr, labels)
448-
case Own(_) : return lift_own(cx, load_int(opts, ptr, 4), t)
449-
case Borrow(_) : return lift_borrow(cx, load_int(opts, ptr, 4), t)
442+
case Own() : return lift_own(cx, load_int(opts, ptr, 4), t)
443+
case Borrow() : return lift_borrow(cx, load_int(opts, ptr, 4), t)
450444
```
451445

452446
Integers are loaded directly from memory, with their high-order bit interpreted
@@ -623,19 +617,31 @@ def unpack_flags_from_int(i, labels):
623617
return record
624618
```
625619

626-
Finally, `own` and `borrow` handles are lifted by loading their referenced resources
627-
out of the current component instance's handle table:
620+
Next, `own` handles are lifted removing the `OwnHandle` from the handle table
621+
and passing the underlying representation value as the lifted value. The other
622+
side will wrap this representation value in its own new `OwnHandle` in its on
623+
handle table.
628624
```python
629625
def lift_own(cx, i, t):
630-
return cx.inst.handles.remove(i, t)
626+
h = cx.inst.handles.remove(i, t)
627+
return OwnHandle(h.rep, 0)
628+
```
629+
Note that `t` refers to an `own` type and thus `HandleTable.remove` will, as
630+
shown above, ensure that the handle at index `i` is an `OwnHandle`.
631631

632+
Lastly, `borrow` handles are lifted by getting the handle out of the
633+
appropriate resource type's handle table.
634+
```python
632635
def lift_borrow(cx, i, t):
633-
return cx.inst.handles.get(i, t.rt)
636+
h = cx.inst.handles.get(i, t.rt)
637+
h.lend_count += 1
638+
cx.lenders.append(h)
639+
return BorrowHandle(h.rep, 0, None)
634640
```
635-
The `remove` method is used in `lift_own` so that passing an `own` handle
636-
across a component boundary transfers ownership. Note that `remove` checks
637-
*both* the resource type tag *and* that the indexed handle is an `OwnHandle`
638-
while `get` only checks the resource type, thereby allowing any handle type.
641+
Thus, `borrow` lifting allows all handle types to be supplied for a `borrow`,
642+
as long as they have a matching resource type (`t.rt`). The lending handle is
643+
passed as the lifted value so that the receiver can increment and decrement
644+
its `lend_count`.
639645

640646

641647
### Storing
@@ -665,8 +671,8 @@ def store(cx, v, t, ptr):
665671
case Record(fields) : store_record(cx, v, ptr, fields)
666672
case Variant(cases) : store_variant(cx, v, ptr, cases)
667673
case Flags(labels) : store_flags(cx, v, ptr, labels)
668-
case Own(rt) : store_int(cx, lower_own(opts, v, rt), ptr, 4)
669-
case Borrow(rt) : store_int(cx, lower_borrow(opts, v, rt), ptr, 4)
674+
case Own() : store_int(cx, lower_own(opts, v, t), ptr, 4)
675+
case Borrow() : store_int(cx, lower_borrow(opts, v, t), ptr, 4)
670676
```
671677

672678
Integers are stored directly into memory. Because the input domain is exactly
@@ -979,18 +985,16 @@ def pack_flags_into_int(v, labels):
979985
Finally, `own` and `borrow` handles are lowered by inserting them into the
980986
current component instance's `HandleTable`:
981987
```python
982-
def lower_own(cx, src, rt):
983-
assert(isinstance(src, OwnHandle))
984-
h = OwnHandle(src.rep, 0)
985-
return cx.inst.handles.insert(h, rt)
986-
987-
def lower_borrow(cx, src, rt):
988-
assert(isinstance(src, Handle))
989-
if cx.inst is rt.impl:
990-
return src.rep
991-
cx.borrow_scope.add(src)
992-
h = BorrowHandle(src.rep, 0, cx.borrow_scope)
993-
return cx.inst.handles.insert(h, rt)
988+
def lower_own(cx, h, t):
989+
assert(isinstance(h, OwnHandle))
990+
return cx.inst.handles.add(h, t)
991+
992+
def lower_borrow(cx, h, t):
993+
assert(isinstance(h, BorrowHandle))
994+
if cx.inst is t.rt.impl:
995+
return h.rep
996+
h.cx = cx
997+
return cx.inst.handles.add(h, t)
994998
```
995999
The special case in `lower_borrow` is an optimization, recognizing that, when
9961000
a borrowed handle is passed to the component that implemented the resource
@@ -1151,8 +1155,8 @@ def lift_flat(cx, vi, t):
11511155
case Record(fields) : return lift_flat_record(cx, vi, fields)
11521156
case Variant(cases) : return lift_flat_variant(cx, vi, cases)
11531157
case Flags(labels) : return lift_flat_flags(vi, labels)
1154-
case Own(_) : return lift_own(cx, vi.next('i32'), t)
1155-
case Borrow(_) : return lift_borrow(cx, vi.next('i32'), t)
1158+
case Own() : return lift_own(cx, vi.next('i32'), t)
1159+
case Borrow() : return lift_borrow(cx, vi.next('i32'), t)
11561160
```
11571161

11581162
Integers are lifted from core `i32` or `i64` values using the signedness of the
@@ -1275,8 +1279,8 @@ def lower_flat(cx, v, t):
12751279
case Record(fields) : return lower_flat_record(cx, v, fields)
12761280
case Variant(cases) : return lower_flat_variant(cx, v, cases)
12771281
case Flags(labels) : return lower_flat_flags(v, labels)
1278-
case Own(rt) : return [Value('i32', lower_own(cx, v, rt))]
1279-
case Borrow(rt) : return [Value('i32', lower_borrow(cx, v, rt))]
1282+
case Own() : return [Value('i32', lower_own(cx, v, t))]
1283+
case Borrow() : return [Value('i32', lower_borrow(cx, v, t))]
12801284
```
12811285

12821286
Since component-level values are assumed in-range and, as previously stated,
@@ -1452,7 +1456,7 @@ def canon_lift(opts, inst, callee, ft, args):
14521456
def post_return():
14531457
if opts.post_return is not None:
14541458
opts.post_return(flat_results)
1455-
cx.borrow_scope.exit()
1459+
trap_if(cx.borrow_count != 0)
14561460

14571461
return (results, post_return)
14581462
```
@@ -1505,6 +1509,9 @@ def canon_lower(opts, inst, callee, calling_import, ft, flat_args):
15051509

15061510
post_return()
15071511

1512+
for h in cx.lenders:
1513+
h.lend_count -= 1
1514+
15081515
if calling_import:
15091516
inst.may_enter = True
15101517

@@ -1566,11 +1573,11 @@ validation specifies:
15661573
currently fixed to be `i32`.
15671574

15681575
Calling `$f` invokes the following function, which creates a resource object
1569-
and inserts it into the current instance's handle table:
1576+
and adds it into the current instance's handle table:
15701577
```python
15711578
def canon_resource_new(inst, rt, rep):
15721579
h = OwnHandle(rep, 0)
1573-
return inst.handles.insert(h, rt)
1580+
return inst.handles.add(h, Own(rt))
15741581
```
15751582

15761583
### `canon resource.drop`
@@ -1588,10 +1595,7 @@ be of type `$t` from the handle table and then, for an `own` handle, calls the
15881595
optional destructor.
15891596
```python
15901597
def canon_resource_drop(inst, t, i):
1591-
h = inst.handles.remove(i, t)
1592-
if isinstance(t, Own) and t.rt.dtor:
1593-
trap_if(not t.rt.impl.may_enter)
1594-
t.rt.dtor(h.rep)
1598+
inst.handles.drop(i, t)
15951599
```
15961600
The `may_enter` guard ensures the non-reentrance [component invariant], since
15971601
a destructor call is analogous to a call to an export.
@@ -1613,7 +1617,8 @@ matches. Note that the "locally-defined" requirement above ensures that only
16131617
the component instance defining a resource can access its representation.
16141618
```python
16151619
def canon_resource_rep(inst, rt, i):
1616-
return inst.handles.get(i, rt).rep
1620+
h = inst.handles.get(i, rt)
1621+
return h.rep
16171622
```
16181623

16191624

0 commit comments

Comments
 (0)