Skip to content

Commit a513df6

Browse files
committed
Loosen the reentrance rules to allow parent components to wrap child components' imports and exports
1 parent 799a7f7 commit a513df6

File tree

3 files changed

+48
-42
lines changed

3 files changed

+48
-42
lines changed

design/mvp/CanonicalABI.md

Lines changed: 39 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1132,9 +1132,12 @@ the outside world through an export.
11321132

11331133
Given the above closure arguments, `canon_lift` is defined:
11341134
```python
1135-
def canon_lift(callee_opts, callee_instance, callee, functype, args):
1136-
trap_if(not callee_instance.may_enter)
1137-
callee_instance.may_enter = False
1135+
def canon_lift(callee_opts, callee_instance, callee, functype, args, called_as_export):
1136+
if called_as_export:
1137+
trap_if(not callee_instance.may_enter)
1138+
callee_instance.may_enter = False
1139+
else:
1140+
assert(not callee_instance.may_enter)
11381141

11391142
assert(callee_instance.may_leave)
11401143
callee_instance.may_leave = False
@@ -1150,7 +1153,8 @@ def canon_lift(callee_opts, callee_instance, callee, functype, args):
11501153
def post_return():
11511154
if callee_opts.post_return is not None:
11521155
callee_opts.post_return(flat_results)
1153-
callee_instance.may_enter = True
1156+
if called_as_export:
1157+
callee_instance.may_enter = True
11541158

11551159
return (result, post_return)
11561160
```
@@ -1161,29 +1165,20 @@ boundaries. Thus, if a component wishes to signal an error, it must use some
11611165
sort of explicit type such as `expected` (whose `error` case particular
11621166
language bindings may choose to map to and from exceptions).
11631167

1164-
The clearing of `may_enter` for the entire duration of `canon_lift` and the
1165-
fact that `canon_lift` brackets all calls into a component ensure that
1166-
components cannot be reentered, which is a [component invariant]. Furthermore,
1167-
because `may_enter` is not cleared on the exceptional exit path taken by
1168-
`trap()`, if there is a trap during Core WebAssembly execution or
1169-
lifting/lowering, the component is left permanently un-enterable, ensuring the
1170-
lockdown-after-trap [component invariant].
1168+
The `called_as_export` parameter indicates whether `canon_lift` is being called
1169+
as part of a component export or whether this `canon_lift` is being called
1170+
internally (for example, by a child component instance). By clearing
1171+
`may_enter` for the duration of `canon_lift` when called as an export, the
1172+
dynamic traps ensure that components cannot be reentered, which is a [component
1173+
invariant]. Furthermore, because `may_enter` is not cleared on the exceptional
1174+
exit path taken by `trap()`, if there is a trap during Core WebAssembly
1175+
execution or lifting/lowering, the component is left permanently un-enterable,
1176+
ensuring the lockdown-after-trap [component invariant].
11711177

11721178
The contract assumed by `canon_lift` (and ensured by `canon_lower` below) is
11731179
that the caller of `canon_lift` *must* call `post_return` right after lowering
1174-
`result`. This ordering ensures that the engine can reliably copy directly from
1175-
the callee's linear memory (read by `lift`) into the caller's linear memory
1176-
(written by `lower`). If `post_return` were called earlier (e.g., before
1177-
`canon_lift` returned), the callee's linear memory would have already been
1178-
freed and so the engine would need to eagerly make an intermediate copy in
1179-
`lift`.
1180-
1181-
The `may_leave` guard wrapping the lowering of parameters conservatively
1182-
ensures that `realloc` calls during lowering do not call imports that
1183-
indirectly re-enter the instance that lifted the same parameters. While the
1184-
`may_enter` guards of *those* component instances would also prevent this
1185-
re-entrance, it would be an error that only manifested in certain component
1186-
linking configurations, hence the eager error helps ensure compositionality.
1180+
`result`. This ensures that `post_return` can be used to perform cleanup
1181+
actions after the lowering is complete.
11871182

11881183

11891184
### `lower`
@@ -1230,19 +1225,26 @@ lifting and lowering), with a few exceptions:
12301225
the caller pass in a pointer to caller-allocated memory as a final
12311226
`i32` parameter.
12321227

1233-
A useful consequence of the above rules for `may_enter` and `may_leave` is that
1234-
attempting to `canon lower` to a `callee` in the same instance is a guaranteed,
1235-
immediate trap which a link-time compiler can eagerly compile to an
1236-
`unreachable`. This avoids what would otherwise be a surprising form of memory
1237-
aliasing that could introduce obscure bugs.
1238-
1239-
The net effect here is that any cross-component call necessarily
1240-
transits through a composed `canon_lower`/`canon_lift` pair, allowing a link-time
1241-
compiler to fuse the lifting/lowering steps of these two definitions into a
1242-
single, efficient trampoline. This fusion model allows efficient compilation of
1243-
the permissive [subtyping](Subtyping.md) allowed between components (including
1244-
the elimination of string operations on the labels of records and variants) as
1245-
well as post-MVP [adapter functions].
1228+
Since any cross-component call necessarily transits through a statically-known
1229+
`canon_lower`+`canon_lift` call pair, an AOT compiler can fuse `canon_lift` and
1230+
`canon_lower` into a single, efficient trampoline. This allows efficient
1231+
compilation of the permissive [subtyping](Subtyping.md) allowed between
1232+
components (including the elimination of string operations on the labels of
1233+
records and variants) as well as post-MVP [adapter functions].
1234+
1235+
The `may_leave` flag set during lowering in `canon_lift` and `canon_lower`
1236+
ensures that the relative ordering of the side effects of `lift` and `lower`
1237+
cannot be observed via import calls and thus an implementation may reliably
1238+
interleave `lift` and `lower` whenever making a cross-component call to avoid
1239+
the intermediate copy performed by `lift`. This unobservability of interleaving
1240+
depends on the shared-nothing property of components which guarantees that all
1241+
the low-level state touched by `lift` and `lower` are disjoint. Though it
1242+
should be rare, same-component-instance `canon_lift`+`canon_lower` call pairs
1243+
are technically allowed by the above rules (and may arise unintentionally in
1244+
component reexport scenarios). Such cases can be statically distinguished by
1245+
the AOT compiler as requiring an intermediate copy to implement the above
1246+
`lift`-then-`lower` semantics.
1247+
12461248

12471249

12481250
[Canonical Definitions]: Explainer.md#canonical-definitions

design/mvp/canonical-abi/definitions.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -870,9 +870,12 @@ class Instance:
870870
may_enter = True
871871
# ...
872872

873-
def canon_lift(callee_opts, callee_instance, callee, functype, args):
874-
trap_if(not callee_instance.may_enter)
875-
callee_instance.may_enter = False
873+
def canon_lift(callee_opts, callee_instance, callee, functype, args, called_as_export):
874+
if called_as_export:
875+
trap_if(not callee_instance.may_enter)
876+
callee_instance.may_enter = False
877+
else:
878+
assert(not callee_instance.may_enter)
876879

877880
assert(callee_instance.may_leave)
878881
callee_instance.may_leave = False
@@ -888,7 +891,8 @@ def canon_lift(callee_opts, callee_instance, callee, functype, args):
888891
def post_return():
889892
if callee_opts.post_return is not None:
890893
callee_opts.post_return(flat_results)
891-
callee_instance.may_enter = True
894+
if called_as_export:
895+
callee_instance.may_enter = True
892896

893897
return (result, post_return)
894898

design/mvp/canonical-abi/run_tests.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,7 @@ def test_roundtrip(t, v):
342342

343343
callee_heap = Heap(1000)
344344
callee_opts = mk_opts(callee_heap.memory, 'utf8', callee_heap.realloc, lambda x: () )
345-
lifted_callee = lambda args: canon_lift(callee_opts, callee_instance, callee, ft, args)
345+
lifted_callee = lambda args: canon_lift(callee_opts, callee_instance, callee, ft, args, True)
346346

347347
caller_heap = Heap(1000)
348348
caller_instance = Instance()

0 commit comments

Comments
 (0)