@@ -220,67 +220,30 @@ The subsequent definitions of loading and storing a value from linear memory
220
220
require additional runtime state, which is threaded through most subsequent
221
221
definitions via the ` cx ` parameter of type ` CallContext ` :
222
222
``` python
223
+ @dataclass
223
224
class CallContext :
224
225
opts: CanonicalOptions
225
226
inst: ComponentInstance
226
- lenders: list[HandleElem]
227
- borrow_count: int
228
-
229
- def __init__ (self , opts , inst ):
230
- self .opts = opts
231
- self .inst = inst
232
- self .lenders = []
233
- self .borrow_count = 0
234
227
```
235
- One ` CallContext ` is created for each call for both the component caller and
236
- callee (defined below in ` canon_lower ` and ` canon_lift ` , resp.). Thus, a
237
- cross-component call will create 2 ` CallContext ` objects for the call, while a
238
- component-to-host or host-to-component call will create a single ` CallContext `
239
- for the component caller or callee, resp.
240
228
241
- The meaning of the ` opts ` and ` inst ` fields are described with their associated
242
- types below.
243
-
244
- The ` lenders ` and ` borrow_count ` fields are used by the following helper
245
- methods of ` CallContext ` plus the ` {lift,lower}_{own,borrow} ` operations. These
246
- fields are updated at the appropriate points in the lifecycle of a call (below)
247
- and maintain the bookkeeping to dynamically ensure that ` own ` handles are not
248
- dropped while they have been ` borrow ` ed and that all ` borrow ` handles created
249
- for a call are dropped before the end of the call.
250
- ``` python
251
- def track_owning_lend (self , lending_handle ):
252
- assert (lending_handle.own)
253
- lending_handle.lend_count += 1
254
- self .lenders.append(lending_handle)
255
-
256
- def exit_call (self ):
257
- trap_if(self .borrow_count != 0 )
258
- for h in self .lenders:
259
- h.lend_count -= 1
260
- ```
261
- Note, the ` lenders ` list usually has a fixed size (in all cases except when a
262
- function signature has ` borrow ` s in ` list ` s) and thus can be stored inline in
263
- the native stack frame.
264
-
265
- The ` CanonicalOptions ` class implements the ` opts ` field of ` CallContext ` and
266
- represents the [ ` canonopt ` ] values supplied to currently-executing ` canon lift `
267
- or ` canon lower ` :
229
+ The ` opts ` field of ` CallContext ` contains all the possible ` canonopt `
230
+ immediates that can be passed to the ` canon ` definition being implemented.
268
231
``` python
232
+ @dataclass
269
233
class CanonicalOptions :
270
- memory: bytearray
271
- string_encoding: str
272
- realloc: Callable[[int ,int ,int ,int ],int ]
273
- post_return: Callable[[],None ]
234
+ memory: Optional[ bytearray ] = None
235
+ string_encoding: Optional[ str ] = None
236
+ realloc: Optional[ Callable[[int ,int ,int ,int ],int ]] = None
237
+ post_return: Optional[ Callable[[],None ]] = None
274
238
```
275
239
276
- The ` ComponentInstance ` class implements the ` inst ` field of ` CallContext ` and
277
- represents the component instance that the currently-executing canonical
278
- definition is defined to execute inside. The ` may_enter ` and ` may_leave ` fields
279
- are used to enforce the [ component invariants] : ` may_leave ` indicates whether
280
- the instance may call out to an import and the ` may_enter ` state indicates
281
- whether the instance may be called from the outside world through an export.
240
+ The ` inst ` field of ` CallContext ` points to the component instance which the
241
+ ` canon ` -generated function is closed over. Component instances contain all the
242
+ core wasm instance as well as some extra state that is used exclusively by the
243
+ Canonical ABI:
282
244
``` python
283
245
class ComponentInstance :
246
+ # core module instance state
284
247
may_leave: bool
285
248
may_enter: bool
286
249
handles: HandleTables
@@ -290,9 +253,14 @@ class ComponentInstance:
290
253
self .may_enter = True
291
254
self .handles = HandleTables()
292
255
```
293
- The ` HandleTables ` object held by ` ComponentInstance ` contains a mapping
294
- from ` ResourceType ` to ` Table ` s of ` HandleElem ` s (defined next), establishing
295
- a separate ` i32 ` -indexed array per resource type.
256
+ The ` may_enter ` and ` may_leave ` fields are used to enforce the [ component
257
+ invariants] : ` may_leave ` indicates whether the instance may call out to an
258
+ import and the ` may_enter ` state indicates whether the instance may be called
259
+ from the outside world through an export.
260
+
261
+ The ` handles ` field of ` ComponentInstance ` contains a mapping from
262
+ ` ResourceType ` to ` Table ` s of ` HandleElem ` s (defined next), establishing a
263
+ separate ` i32 ` -indexed array per resource type.
296
264
``` python
297
265
class HandleTables :
298
266
rt_to_table: MutableMapping[ResourceType, Table[HandleElem]]
@@ -392,7 +360,7 @@ stored in `HandleTables`:
392
360
class HandleElem :
393
361
rep: int
394
362
own: bool
395
- scope: Optional[CallContext ]
363
+ scope: Optional[ExportCall ]
396
364
lend_count: int
397
365
398
366
def __init__ (self , rep , own , scope = None ):
@@ -407,15 +375,14 @@ fixed to be an `i32`) passed to `resource.new`.
407
375
The ` own ` field indicates whether this element was created from an ` own ` type
408
376
(or, if false, a ` borrow ` type).
409
377
410
- The ` scope ` field optionally stores the ` CallContext ` of the call that created
411
- this handle if the handle type was ` borrow ` . Until async is added to the
412
- Component Model, because of the non-reentrancy of components, there is at most
413
- one ` CallContext ` alive for a given component at a time and thus this field
414
- does not actually need to be stored per ` HandleElem ` .
378
+ The ` scope ` field stores the ` ExportCall ` that created the borrowed handle.
379
+ Until async is added to the Component Model, because of the non-reentrancy of
380
+ components, there is at most one ` ExportCall ` alive for a given component at a
381
+ time and thus this field does not actually need to be stored per ` HandleElem ` .
415
382
416
383
The ` lend_count ` field maintains a conservative approximation of the number of
417
384
live handles that were lent from this ` own ` handle (by calls to ` borrow ` -taking
418
- functions). This count is maintained by the ` CallContext ` bookkeeping functions
385
+ functions). This count is maintained by the ` ImportCall ` bookkeeping functions
419
386
(above) and is ensured to be zero when an ` own ` handle is dropped.
420
387
421
388
An optimizing implementation can enumerate the canonical definitions present
@@ -424,6 +391,64 @@ table only contains `own` or `borrow` handles and then, based on this,
424
391
statically eliminate the ` own ` and the ` lend_count ` xor ` scope ` fields,
425
392
and guards thereof.
426
393
394
+ One ` CallContext ` is created for each call for both the component caller and
395
+ callee (in ` canon_lower ` and ` canon_lift ` , resp., as defined below). Thus, a
396
+ cross-component call will create 2 ` CallContext ` objects for the call, while a
397
+ component-to-host or host-to-component call will create a single ` CallContext `
398
+ for the component caller or callee, resp.
399
+
400
+ Additional per-call state is required to check that callers and callees uphold
401
+ their respective parts of the call contract:
402
+
403
+ The ` ExportCall ` subclass of ` CallContext ` tracks the number of borrowed
404
+ handles that were passed as parameters to the export that have not yet been
405
+ dropped (which might dangle if the caller destroys the resource after the
406
+ call):
407
+ ``` python
408
+ class ExportCall (CallContext ):
409
+ borrow_count: int
410
+
411
+ def __init__ (self , opts , inst ):
412
+ super ().__init__ (opts, inst)
413
+ self .borrow_count = 0
414
+
415
+ def create_borrow (self ):
416
+ self .borrow_count += 1
417
+
418
+ def drop_borrow (self ):
419
+ assert (self .borrow_count > 0 )
420
+ self .borrow_count -= 1
421
+
422
+ def exit (self ):
423
+ trap_if(self .borrow_count != 0 )
424
+ ```
425
+
426
+ The ` ImportCall ` subclass of ` CallContext ` tracks the owned handles that have
427
+ been lent for the duration of an import call, ensuring that they aren't dropped
428
+ during the call (which might create a dangling borrowed handle):
429
+ ``` python
430
+ class ImportCall (CallContext ):
431
+ lenders: list[HandleElem]
432
+
433
+ def __init__ (self , opts , inst ):
434
+ assert (inst.entered)
435
+ super ().__init__ (opts, inst)
436
+ self .lenders = []
437
+
438
+ def track_owning_lend (self , lending_handle ):
439
+ assert (lending_handle.own)
440
+ lending_handle.lend_count += 1
441
+ self .lenders.append(lending_handle)
442
+
443
+ def exit (self ):
444
+ assert (self .inst.entered)
445
+ for h in self .lenders:
446
+ h.lend_count -= 1
447
+ ```
448
+ Note, the ` lenders ` list usually has a fixed size (in all cases except when a
449
+ function signature has ` borrow ` s in ` list ` s) and thus can be stored inline in
450
+ the native stack frame.
451
+
427
452
428
453
### Loading
429
454
@@ -670,6 +695,7 @@ from the source handle, leaving the source handle intact in the current
670
695
component instance's handle table:
671
696
``` python
672
697
def lift_borrow (cx , i , t ):
698
+ assert (isinstance (cx, ImportCall))
673
699
h = cx.inst.handles.get(t.rt, i)
674
700
if h.own:
675
701
cx.track_owning_lend(h)
@@ -1071,10 +1097,11 @@ def lower_own(cx, rep, t):
1071
1097
return cx.inst.handles.add(t.rt, h)
1072
1098
1073
1099
def lower_borrow (cx , rep , t ):
1100
+ assert (isinstance (cx, ExportCall))
1074
1101
if cx.inst is t.rt.impl:
1075
1102
return rep
1076
1103
h = HandleElem(rep, own = False , scope = cx)
1077
- cx.borrow_count += 1
1104
+ cx.create_borrow()
1078
1105
return cx.inst.handles.add(t.rt, h)
1079
1106
```
1080
1107
The special case in ` lower_borrow ` is an optimization, recognizing that, when
@@ -1083,7 +1110,6 @@ type, the only thing the borrowed handle is good for is calling
1083
1110
` resource.rep ` , so lowering might as well avoid the overhead of creating an
1084
1111
intermediate borrow handle.
1085
1112
1086
-
1087
1113
### Flattening
1088
1114
1089
1115
With only the definitions above, the Canonical ABI would be forced to place all
@@ -1460,6 +1486,10 @@ greater-than-`max_flat` case by either allocating storage with `realloc` or
1460
1486
accepting a caller-allocated buffer as an out-param:
1461
1487
``` python
1462
1488
def lower_values (cx , max_flat , vs , ts , out_param = None ):
1489
+ inst = cx.inst
1490
+ assert (inst.may_leave)
1491
+ inst.may_leave = False
1492
+
1463
1493
flat_types = flatten_types(ts)
1464
1494
if len (flat_types) > max_flat:
1465
1495
tuple_type = Tuple(ts)
@@ -1471,13 +1501,21 @@ def lower_values(cx, max_flat, vs, ts, out_param = None):
1471
1501
trap_if(ptr != align_to(ptr, alignment(tuple_type)))
1472
1502
trap_if(ptr + elem_size(tuple_type) > len (cx.opts.memory))
1473
1503
store(cx, tuple_value, tuple_type, ptr)
1474
- return [ptr]
1504
+ flat_vales = [ptr]
1475
1505
else :
1476
1506
flat_vals = []
1477
1507
for i in range (len (vs)):
1478
1508
flat_vals += lower_flat(cx, vs[i], ts[i])
1479
- return flat_vals
1509
+
1510
+ inst.may_leave = True
1511
+ return flat_vals
1480
1512
```
1513
+ The ` may_leave ` flag is used by ` canon_lower ` below to prevent a component from
1514
+ calling out of the component while in the middle of lowering, ensuring that the
1515
+ relative ordering of the side effects of ` lift_values ` and ` lower_values `
1516
+ cannot be observed and thus an implementation may reliably fuse ` lift_values `
1517
+ with ` lower_values ` when making a cross-component call, avoiding any
1518
+ intermediate copy.
1481
1519
1482
1520
## Canonical Definitions
1483
1521
@@ -1518,27 +1556,25 @@ component*.
1518
1556
Given the above closure arguments, ` canon_lift ` is defined:
1519
1557
``` python
1520
1558
def canon_lift (opts , inst , callee , ft , args ):
1521
- cx = CallContext (opts, inst)
1559
+ export_call = ExportCall (opts, inst)
1522
1560
trap_if(not inst.may_enter)
1523
1561
1524
- assert (inst.may_leave)
1525
- inst.may_leave = False
1526
- flat_args = lower_values(cx, MAX_FLAT_PARAMS , args, ft.param_types())
1527
- inst.may_leave = True
1528
-
1529
- try :
1530
- flat_results = callee(flat_args)
1531
- except CoreWebAssemblyException:
1532
- trap()
1533
-
1534
- results = lift_values(cx, MAX_FLAT_RESULTS , CoreValueIter(flat_results), ft.result_types())
1562
+ flat_args = lower_values(export_call, MAX_FLAT_PARAMS , args, ft.param_types())
1563
+ flat_results = call_and_trap_on_throw(callee, flat_args)
1564
+ results = lift_values(export_call, MAX_FLAT_RESULTS , CoreValueIter(flat_results), ft.result_types())
1535
1565
1536
1566
def post_return ():
1537
1567
if opts.post_return is not None :
1538
- opts.post_return( flat_results)
1539
- cx.exit_call ()
1568
+ call_and_trap_on_throw( opts.post_return, flat_results)
1569
+ export_call.exit ()
1540
1570
1541
1571
return (results, post_return)
1572
+
1573
+ def call_and_trap_on_throw (callee , args ):
1574
+ try :
1575
+ return callee(args)
1576
+ except CoreWebAssemblyException:
1577
+ trap()
1542
1578
```
1543
1579
Uncaught Core WebAssembly [ exceptions] result in a trap at component
1544
1580
boundaries. Thus, if a component wishes to signal an error, it must use some
@@ -1571,24 +1607,22 @@ containing a `hostfunc` that closes over `$opts`, `$inst`, `$callee` and `$ft`
1571
1607
and, when called from Core WebAssembly code, calls ` canon_lower ` , which is defined as:
1572
1608
``` python
1573
1609
def canon_lower (opts , inst , callee , calling_import , ft , flat_args ):
1574
- cx = CallContext (opts, inst)
1610
+ import_call = ImportCall (opts, inst)
1575
1611
trap_if(not inst.may_leave)
1576
1612
1577
1613
assert (inst.may_enter)
1578
1614
if calling_import:
1579
1615
inst.may_enter = False
1580
1616
1581
1617
flat_args = CoreValueIter(flat_args)
1582
- args = lift_values(cx , MAX_FLAT_PARAMS , flat_args, ft.param_types())
1618
+ args = lift_values(import_call , MAX_FLAT_PARAMS , flat_args, ft.param_types())
1583
1619
1584
1620
results, post_return = callee(args)
1585
1621
1586
- inst.may_leave = False
1587
- flat_results = lower_values(cx, MAX_FLAT_RESULTS , results, ft.result_types(), flat_args)
1588
- inst.may_leave = True
1622
+ flat_results = lower_values(import_call, MAX_FLAT_RESULTS , results, ft.result_types(), flat_args)
1589
1623
1590
1624
post_return()
1591
- cx.exit_call ()
1625
+ import_call.exit ()
1592
1626
1593
1627
if calling_import:
1594
1628
inst.may_enter = True
@@ -1626,20 +1660,6 @@ Because `may_enter` is not cleared on the exceptional exit path taken by
1626
1660
lowering, the component is left permanently un-enterable, ensuring the
1627
1661
lockdown-after-trap [ component invariant] .
1628
1662
1629
- The ` may_leave ` flag set during lowering in ` canon_lift ` and ` canon_lower `
1630
- ensures that the relative ordering of the side effects of ` lift ` and ` lower `
1631
- cannot be observed via import calls and thus an implementation may reliably
1632
- interleave ` lift ` and ` lower ` whenever making a cross-component call to avoid
1633
- the intermediate copy performed by ` lift ` . This unobservability of interleaving
1634
- depends on the shared-nothing property of components which guarantees that all
1635
- the low-level state touched by ` lift ` and ` lower ` are disjoint. Though it
1636
- should be rare, same-component-instance ` canon_lift ` +` canon_lower ` call pairs
1637
- are technically allowed by the above rules (and may arise unintentionally in
1638
- component reexport scenarios). Such cases can be statically distinguished by
1639
- the AOT compiler as requiring an intermediate copy to implement the above
1640
- ` lift ` -then-` lower ` semantics.
1641
-
1642
-
1643
1663
### ` canon resource.new `
1644
1664
1645
1665
For a canonical definition:
@@ -1684,9 +1704,7 @@ def canon_resource_drop(inst, rt, i):
1684
1704
if rt.dtor:
1685
1705
rt.dtor(h.rep)
1686
1706
else :
1687
- assert (h.scope is not None )
1688
- assert (h.scope.borrow_count > 0 )
1689
- h.scope.borrow_count -= 1
1707
+ h.scope.drop_borrow()
1690
1708
return []
1691
1709
```
1692
1710
The ` may_enter ` guard ensures the non-reentrance [ component invariant] , since
0 commit comments