@@ -119,41 +119,71 @@ runtime behavior of the Canonical ABI, the Embedding interface here just
119119includes functions for the embedder to:
1201201 . construct a Component Model ` Store ` , analogous to [ ` store_init ` ] ing a Core
121121 WebAssembly [ ` store ` ] ;
122- 2 . ` invoke ` a Component Model ` FuncInst ` , analogous to [ ` func_invoke ` ] ing a
123- Core WebAssembly [ ` funcinst ` ] ; and
124- 3 . allow a cooperative thread (created during a previous call to to ` invoke ` )
125- to execute until blocking or exiting.
126-
122+ 2 . ` Store.invoke ` a Component Model ` FuncInst ` , analogous to [ ` func_invoke ` ] ing
123+ a Core WebAssembly [ ` funcinst ` ] ; and
124+ 3 . allow an existing thread to execute, via ` Store.tick ` , until [ blocking] or
125+ exiting.
126+
127+ ` Store.tick ` does not have an analogue in Core WebAssembly and is necessary to
128+ provide native [ concurrency] support in the Component Model. The expectation is
129+ that the host will interleave calls to ` invoke ` with calls to ` tick ` , repeatedly
130+ calling ` tick ` until there is no more work to do or the store is destroyed.
131+ ` tick ` never blocks: if execution * would* block, the thread is suspended and
132+ execution returns to the host from ` tick ` (which can then call ` invoke ` or
133+ ` tick ` again, etc). Although the host would use more sophisticated data
134+ structures for performance, the definition of ` tick ` below simply tracks all the
135+ threads that are ` waiting ` to run once some ` ready ` condition is met in one big
136+ list and then nondeterministically picks one. The ` Thread.ready ` and
137+ ` Thread.resume ` methods along with how the ` Store.waiting ` list is populated are
138+ all defined below as part of the ` Thread ` class.
139+
140+ Hosts are allowed to call ` Store.invoke ` recursively, i.e., if a component is
141+ instantiated with host functions, the host is allowed to recursively call
142+ ` Store.invoke ` again, producing a synchronous callstack:
143+ ```
144+ [host] --invoke--> [component] --call-import--> [host] --invoke--> [component]
145+ ```
146+ Hosts are * not* allowed to call ` Store.tick ` recursively; it can only be called
147+ from the base of the call stack. However, once component code is running from
148+ a ` Store.tick ` , the host can recursively call ` Store.invoke ` as above:
149+ ```
150+ [host] --tick--> [component] --call-import--> [host] --invoke--> [component]
151+ ```
152+ Furthermore, hosts are required to link the two ` [component] ` calls in the above
153+ call stacks together, by passing the ` Supertask ` of the older ` [component] ` call
154+ to the inner ` Store.invoke ` .
155+
156+ The above host requirements are ` assert() ` ed by the ` nesting_depth ` accounting
157+ below which, depending on the embedding scenario, may or may not need to be
158+ enforced at runtime by the engine.
127159``` python
128160class Store :
129161 waiting: list[Thread]
162+ nesting_depth: int
130163
131164 def __init__ (self ):
132165 self .waiting = []
166+ self .nesting_depth = 0
133167
134168 def invoke (self , f : FuncInst, caller : Optional[Supertask], on_start , on_resolve ) -> Call:
135169 host_caller = Supertask()
136170 host_caller.inst = None
137171 host_caller.supertask = caller
138- return f(host_caller, on_start, on_resolve)
172+ self .nesting_depth += 1
173+ call = f(host_caller, on_start, on_resolve)
174+ self .nesting_depth -= 1
175+ return call
139176
140177 def tick (self ):
178+ assert (self .nesting_depth == 0 )
179+ self .nesting_depth = 1
141180 random.shuffle(self .waiting)
142181 for thread in self .waiting:
143182 if thread.ready():
144183 thread.resume(Cancelled.FALSE )
145- return
146- ```
147- The ` Store.tick ` method does not have an analogue in Core WebAssembly and
148- enables [ native concurrency support] ( Concurrency.md ) in the Component Model. The
149- expectation is that the host will interleave calls to ` invoke ` with calls to
150- ` tick ` , repeatedly calling ` tick ` until there is no more work to do or the
151- store is destroyed. The nondeterministic ` random.shuffle ` indicates that the
152- embedder is allowed to use any algorithm (involving priorities, fairness, etc)
153- to choose which thread to schedule next (and hopefully an algorithm more
154- efficient than the simple polling loop written above). The ` Thread.ready ` and
155- ` Thread.resume ` methods along with how the ` waiting ` list is populated are all
156- defined [ below] ( #thread-state ) as part of the ` Thread ` class.
184+ break
185+ self .nesting_depth = 0
186+ ```
157187
158188The ` FuncInst ` passed to ` Store.invoke ` is defined to take 3 parameters:
159189* an optional ` caller ` ` Supertask ` which is used to maintain the
@@ -330,6 +360,7 @@ class ComponentInstance:
330360 handles: Table[ResourceHandle | Waitable | WaitableSet | ErrorContext]
331361 threads: Table[Thread]
332362 may_leave: bool
363+ may_block: bool
333364 backpressure: int
334365 exclusive: Optional[Task]
335366 num_waiting_to_enter: int
@@ -341,6 +372,7 @@ class ComponentInstance:
341372 self .handles = Table()
342373 self .threads = Table()
343374 self .may_leave = True
375+ self .may_block = True
344376 self .backpressure = 0
345377 self .exclusive = None
346378 self .num_waiting_to_enter = 0
@@ -881,7 +913,7 @@ here which transfers control flow back to `Thread.resume()` via `block()`. The
881913` switch_to ` argument ` None ` tells ` Thread.resume() ` to return immediately.
882914``` python
883915 def suspend (self , cancellable ) -> Cancelled:
884- assert (self .running() and self .task.may_block() )
916+ assert (self .running() and self .task.inst. may_block)
885917 if self .task.deliver_pending_cancel(cancellable):
886918 return Cancelled.TRUE
887919 self .cancellable = cancellable
@@ -903,7 +935,7 @@ until a particular condition is met, as specified by the boolean-valued
903935` ready_func ` parameter:
904936``` python
905937 def wait_until (self , ready_func , cancellable = False ) -> Cancelled:
906- assert (self .running() and self .task.may_block() )
938+ assert (self .running() and self .task.inst. may_block)
907939 if self .task.deliver_pending_cancel(cancellable):
908940 return Cancelled.TRUE
909941 if ready_func() and not DETERMINISTIC_PROFILE and random.randint(0 ,1 ):
@@ -930,7 +962,7 @@ emulating preemptive multi-threading.
930962``` python
931963 def yield_until (self , ready_func , cancellable ) -> Cancelled:
932964 assert (self .running())
933- if self .task.may_block() :
965+ if self .task.inst. may_block:
934966 return self .wait_until(ready_func, cancellable)
935967 else :
936968 assert (ready_func())
@@ -1175,14 +1207,6 @@ synchronously or with `async callback`. This predicate is used by the other
11751207 return not self .opts.async_ or self .opts.callback
11761208```
11771209
1178- The ` Task.may_block ` predicate returns whether the [ current task] 's function's
1179- type is allowed to [ block] . Specifically, functions that do not declare the
1180- ` async ` effect that have not yet returned a value may not block.
1181- ``` python
1182- def may_block (self ):
1183- return self .ft.async_ or self .state == Task.State.RESOLVED
1184- ```
1185-
11861210The ` Task.enter ` method implements [ backpressure] between when the caller of an
11871211` async ` -typed function initiates the call and when the callee's core wasm entry
11881212point is executed. This interstitial placement allows a component instance that
@@ -1204,7 +1228,14 @@ they may reenter core wasm when an `async`-typed function would have been
12041228blocked by implicit backpressure. Thus, export bindings generators must be
12051229careful to handle this possibility (e.g., while maintaining the linear-memory
12061230shadow stack pointer) for components with mixed ` async ` - and non-` async ` - typed
1207- exports.
1231+ exports. However, non-` async ` -typed functions * must* return a value before
1232+ blocking, and thus the ` may_block ` flag (which is dynamically checked before
1233+ all Canonical ABI built-ins which might block) is cleared when entering a
1234+ non-` async ` -typed function, to be cleared in ` Task.return_ ` . Because recursive
1235+ calls to ` Store.tick ` and to the same component instance are disallowed (see
1236+ ` Store ` and ` call_might_be_recursive ` , resp.) and because reentrance is only
1237+ otherwise possible when blocked, ` may_block ` must always already be true when
1238+ ` Task.enter ` is called.
12081239``` python
12091240 def enter (self ):
12101241 thread = current_thread()
@@ -1223,6 +1254,9 @@ exports.
12231254 if self .needs_exclusive():
12241255 assert (self .inst.exclusive is None )
12251256 self .inst.exclusive = self
1257+ else :
1258+ assert (self .inst.may_block)
1259+ self .inst.may_block = False
12261260 self .register_thread(thread)
12271261 return True
12281262
@@ -1321,15 +1355,19 @@ by `Task.deliver_pending_cancel`, which is checked at all cancellation points:
13211355
13221356The ` Task.return_ ` method is called by ` canon_task_return ` and ` canon_lift ` to
13231357return a list of lifted values to the task's caller via the ` OnResolve `
1324- callback. There is a dynamic error if the callee has not dropped all borrowed
1325- handles by the time ` task.return ` is called which means that the caller can
1326- assume that all its lent handles have been returned to it when it receives the
1327- ` SUBTASK ` ` RETURNED ` event. Note that the initial ` trap_if ` allows a task to
1358+ callback. After returning a value, a synchronous (non-` async ` -typed) function
1359+ is allowed to block. There is a dynamic error if the callee has not dropped all
1360+ borrowed handles by the time ` task.return ` is called which means that the caller
1361+ can assume that all its lent handles have been returned to it when it receives
1362+ the ` SUBTASK ` ` RETURNED ` event. Note that the initial ` trap_if ` allows a task to
13281363return a value even after cancellation has been requested.
13291364``` python
13301365 def return_ (self , result ):
13311366 trap_if(self .state == Task.State.RESOLVED )
13321367 trap_if(self .num_borrows > 0 )
1368+ if not self .ft.async_:
1369+ assert (not self .inst.may_block)
1370+ self .inst.may_block = True
13331371 assert (result is not None )
13341372 self .on_resolve(result)
13351373 self .state = Task.State.RESOLVED
@@ -1347,6 +1385,7 @@ call `task.cancel`.
13471385 def cancel (self ):
13481386 trap_if(self .state != Task.State.CANCEL_DELIVERED )
13491387 trap_if(self .num_borrows > 0 )
1388+ assert (self .ft.async_)
13501389 self .on_resolve(None )
13511390 self .state = Task.State.RESOLVED
13521391```
@@ -3517,7 +3556,7 @@ function (specified as a `funcidx` immediate in `canon lift`) until the
35173556 else :
35183557 event = (EventCode.NONE , 0 , 0 )
35193558 case CallbackCode.WAIT :
3520- trap_if(not task .may_block() )
3559+ trap_if(not inst .may_block)
35213560 wset = inst.handles.get(si)
35223561 trap_if(not isinstance (wset, WaitableSet))
35233562 event = wset.wait_until(lambda : not inst.exclusive, cancellable = True )
@@ -3555,16 +3594,19 @@ tasks, which entirely ignore `ComponentInstance.exclusive`.
35553594The end of ` canon_lift ` immediately runs the ` thread_func ` function (which
35563595contains all the steps above) in a new ` Thread ` , allowing ` thread_func ` to make
35573596as much progress as it can before blocking (which transitively calls
3558- ` Thread.suspend ` , deterministically returning control flow here and then to the
3597+ ` block ` , deterministically returning control flow here and then to the
35593598caller. If ` thread_func ` and the core wasm ` callee ` return a value (by calling
35603599the ` OnResolve ` callback) before blocking, the call will complete synchronously
3561- even for ` async ` callers. Note that if an ` async ` callee calls ` OnResolve ` and
3562- * then* blocks, the caller will see the call complete synchronously even though
3563- the callee is still running concurrently in the ` Thread ` created here (see
3564- the [ concurrency explainer] for more on this).
3600+ from the perspective of the caller (even if the callee then blocks * after*
3601+ returning a value, taking advantage of the fact that the callee is running on
3602+ its own ` Thread ` ). By ABI contract, a callee can only block before returning
3603+ its value if it declares the ` async ` effect on the component function type. If a
3604+ callee is non-` async ` -typed and * tries* to block, the callee will trap on the
3605+ ` ComponentInstance.may_block ` guards that dominate all calls to ` block ` .
35653606``` python
35663607 thread = Thread(task, thread_func)
35673608 thread.resume(Cancelled.FALSE )
3609+ assert (ft.async_ or task.state == Task.State.RESOLVED )
35683610 return task
35693611```
35703612
@@ -3631,7 +3673,7 @@ this, `canon_lower` is defined in chunks as follows:
36313673def canon_lower (opts , ft , callee : FuncInst, flat_args ):
36323674 thread = current_thread()
36333675 trap_if(not thread.task.inst.may_leave)
3634- trap_if(not thread.task.may_block() and ft.async_ and not opts.async_)
3676+ trap_if(not thread.task.inst. may_block and ft.async_ and not opts.async_)
36353677```
36363678A non-` async ` -typed function export that has not yet returned a value
36373679unconditionally traps if it transitively attempts to make a synchronous call to
@@ -4095,13 +4137,13 @@ on a `Waitable` in the given waitable set (indicated by index `$si`) and then
40954137returning its ` EventCode ` and writing the payload values into linear memory:
40964138``` python
40974139def canon_waitable_set_wait (cancellable , mem , si , ptr ):
4098- task = current_thread().task
4099- trap_if(not task. inst.may_leave)
4100- trap_if(not task .may_block() )
4101- wset = task. inst.handles.get(si)
4140+ inst = current_thread().task.inst
4141+ trap_if(not inst.may_leave)
4142+ trap_if(not inst .may_block)
4143+ wset = inst.handles.get(si)
41024144 trap_if(not isinstance (wset, WaitableSet))
41034145 event = wset.wait(cancellable)
4104- return unpack_event(mem, task. inst, ptr, event)
4146+ return unpack_event(mem, inst, ptr, event)
41054147
41064148def unpack_event (mem , inst , ptr , e : EventTuple):
41074149 event, p1, p2 = e
@@ -4245,7 +4287,7 @@ BLOCKED = 0xffff_ffff
42454287def canon_subtask_cancel (async_ , i ):
42464288 thread = current_thread()
42474289 trap_if(not thread.task.inst.may_leave)
4248- trap_if(not thread.task.may_block() and not async_)
4290+ trap_if(not thread.task.inst. may_block and not async_)
42494291 subtask = thread.task.inst.handles.get(i)
42504292 trap_if(not isinstance (subtask, Subtask))
42514293 trap_if(subtask.resolve_delivered())
@@ -4379,7 +4421,7 @@ whether the operation would have succeeded eagerly without blocking).
43794421def stream_copy (EndT , BufferT , event_code , stream_t , opts , i , ptr , n ):
43804422 thread = current_thread()
43814423 trap_if(not thread.task.inst.may_leave)
4382- trap_if(not thread.task.may_block() and not opts.async_)
4424+ trap_if(not thread.task.inst. may_block and not opts.async_)
43834425```
43844426
43854427Next, ` stream_copy ` checks that the element at index ` i ` is of the right type
@@ -4501,7 +4543,7 @@ parameters `i` and `ptr`. The only difference is that, with futures, the
45014543def future_copy (EndT , BufferT , event_code , future_t , opts , i , ptr ):
45024544 thread = current_thread()
45034545 trap_if(not thread.task.inst.may_leave)
4504- trap_if(not thread.task.may_block() and not opts.async_)
4546+ trap_if(not thread.task.inst. may_block and not opts.async_)
45054547
45064548 e = thread.task.inst.handles.get(i)
45074549 trap_if(not isinstance (e, EndT))
@@ -4587,7 +4629,7 @@ def canon_future_cancel_write(future_t, async_, i):
45874629def cancel_copy (EndT , event_code , stream_or_future_t , async_ , i ):
45884630 thread = current_thread()
45894631 trap_if(not thread.task.inst.may_leave)
4590- trap_if(not thread.task.may_block() and not async_)
4632+ trap_if(not thread.task.inst. may_block and not async_)
45914633 e = thread.task.inst.handles.get(i)
45924634 trap_if(not isinstance (e, EndT))
45934635 trap_if(e.shared.t != stream_or_future_t.t)
@@ -4783,7 +4825,7 @@ calling component.
47834825def canon_thread_suspend (cancellable ):
47844826 thread = current_thread()
47854827 trap_if(not thread.task.inst.may_leave)
4786- trap_if(not thread.task.may_block() )
4828+ trap_if(not thread.task.inst. may_block)
47874829 cancelled = thread.suspend(cancellable)
47884830 return [cancelled]
47894831```
@@ -5102,6 +5144,7 @@ def canon_thread_available_parallelism():
51025144[ JavaScript Embedding ] : Explainer.md#JavaScript-embedding
51035145[ Adapter Functions ] : FutureFeatures.md#custom-abis-via-adapter-functions
51045146[ Shared-Everything Dynamic Linking ] : examples/SharedEverythingDynamicLinking.md
5147+ [ Concurrency ] : Concurrency.md
51055148[ Concurrency Explainer ] : Concurrency.md
51065149[ Suspended ] : Concurrency.md#thread-built-ins
51075150[ Thread Index ] : Concurrency.md#thread-built-ins
@@ -5112,6 +5155,7 @@ def canon_thread_available_parallelism():
51125155[ Thread ] : Concurrency.md#threads-and-tasks
51135156[ Current Thread ] : Concurrency.md#current-thread-and-task
51145157[ Current Task ] : Concurrency.md#current-thread-and-task
5158+ [ Blocking ] : Concurrency.md#blocking
51155159[ Block ] : Concurrency.md#blocking
51165160[ Waiting On External I/O And Yielding ] : Concurrency.md#blocking
51175161[ Subtasks ] : Concurrency.md#subtasks-and-supertasks
0 commit comments