Problem
Some resources can only be touched from the main interpreter: GUI toolkits (pyglet, Tk, Qt), GL contexts, asyncio loops, SDL surfaces, audio devices, anything backed by a thread-affine C library. Today bocpy has no way to model "this behavior must run on main." @when always dispatches to a sub-interpreter worker, and nothing in the public surface signals "this cown holds a value that must stay on the main interpreter." Users currently smuggle these handles via module globals, which races as soon as a worker tries to touch them — and breaks outright once cross-interpreter transfer refuses to serialise the handle.
The shape of the problem matters. It is not enough to add an escape hatch that runs a callback on main. The resource needs to live inside the bocpy concurrency model — referenced like any other cown, composed with other cowns in the same @when, with the same exception-routing and lifecycle semantics users already expect.
Desired functionality
A Cown-like type whose value lives on, and is only ever touched from, the main interpreter. A behavior whose request set contains any such cown runs on the main interpreter and is free to mix it with ordinary cowns in the same request set. wait() keeps working for batch-style programs; an interactive program that owns its own event loop (pyglet, asyncio) drives the main-side work queue cooperatively from inside that loop.
Hard constraints
- Pinned values are created, held, and accessed only on the main interpreter. Workers never see them.
- Pinned values do not cross interpreters. Behaviors that need both pinned and ordinary cowns run on main, and the ordinary cowns are unwrapped locally.
- A behavior's request set containing any pinned cown forces the behavior to run on main.
- The main thread does not block in a runtime-owned pop loop. Pumping is cooperative with whatever event loop the user runs.
wait() keeps draining a pinned-only program automatically, so existing batch scripts keep working.
- Exceptions raised by a main-side behavior land on the result cown by default, same as the worker path.
- Re-entrant pumping from inside a main-side behavior is not supported in v1 and must raise.
Design questions to resolve
- The exact contract of the cooperative pump entry point: how many behaviors per call, timeout semantics, what it returns.
- Whether pinned cowns reuse the existing
Cown type or introduce a subclass, and the consequences for isinstance checks in user code.
- How the runtime surfaces a diagnostic when an interactive program forgets to pump — the terminator never drains and the user needs to find out without staring at a hung process.
- Whether
start(workers=0) plus pinned-only programs is a supported configuration.
- Whether asyncio integration ships in this work or is left as a follow-on.
Cross-cutting concerns
- Noticeboard snapshot caching across main-side behaviors.
send and receive from inside a main-side behavior — allowed, with blocking semantics documented.
- Refusing cross-interpreter serialisation of a pinned cown with a clear error.
Out of scope
Per-thread affinity beyond "main vs. workers" (no NUMA, no per-worker pinning). Generalising pinning into a capability system. Migration of any existing example or test off the global-handle pattern.
Problem
Some resources can only be touched from the main interpreter: GUI toolkits (pyglet, Tk, Qt), GL contexts, asyncio loops, SDL surfaces, audio devices, anything backed by a thread-affine C library. Today bocpy has no way to model "this behavior must run on main."
@whenalways dispatches to a sub-interpreter worker, and nothing in the public surface signals "this cown holds a value that must stay on the main interpreter." Users currently smuggle these handles via module globals, which races as soon as a worker tries to touch them — and breaks outright once cross-interpreter transfer refuses to serialise the handle.The shape of the problem matters. It is not enough to add an escape hatch that runs a callback on main. The resource needs to live inside the bocpy concurrency model — referenced like any other cown, composed with other cowns in the same
@when, with the same exception-routing and lifecycle semantics users already expect.Desired functionality
A
Cown-like type whose value lives on, and is only ever touched from, the main interpreter. A behavior whose request set contains any such cown runs on the main interpreter and is free to mix it with ordinary cowns in the same request set.wait()keeps working for batch-style programs; an interactive program that owns its own event loop (pyglet, asyncio) drives the main-side work queue cooperatively from inside that loop.Hard constraints
wait()keeps draining a pinned-only program automatically, so existing batch scripts keep working.Design questions to resolve
Cowntype or introduce a subclass, and the consequences forisinstancechecks in user code.start(workers=0)plus pinned-only programs is a supported configuration.Cross-cutting concerns
sendandreceivefrom inside a main-side behavior — allowed, with blocking semantics documented.Out of scope
Per-thread affinity beyond "main vs. workers" (no NUMA, no per-worker pinning). Generalising pinning into a capability system. Migration of any existing example or test off the global-handle pattern.