glowcap 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. glowcap/__init__.py +189 -0
  2. glowcap/_abi_value.py +308 -0
  3. glowcap/_action.py +145 -0
  4. glowcap/_archive_contract.py +25 -0
  5. glowcap/_carrier_failure.py +69 -0
  6. glowcap/_constants.py +80 -0
  7. glowcap/_constants_gen.py +220 -0
  8. glowcap/_context.py +239 -0
  9. glowcap/_dial.py +399 -0
  10. glowcap/_driver/__init__.py +1 -0
  11. glowcap/_driver/finalizer.py +109 -0
  12. glowcap/_driver/release.py +73 -0
  13. glowcap/_driver/transport.py +39 -0
  14. glowcap/_driver_event.py +52 -0
  15. glowcap/_driver_failure.py +143 -0
  16. glowcap/_errors.py +189 -0
  17. glowcap/_failure.py +219 -0
  18. glowcap/_ffi.py +153 -0
  19. glowcap/_ffi_driver.py +138 -0
  20. glowcap/_ffi_host.py +217 -0
  21. glowcap/_ffi_inproc.py +39 -0
  22. glowcap/_ffi_iroh.py +90 -0
  23. glowcap/_ffi_ocapn.py +278 -0
  24. glowcap/_ffi_onion.py +67 -0
  25. glowcap/_ffi_relay.py +50 -0
  26. glowcap/_ffi_tcp.py +55 -0
  27. glowcap/_ffi_webrtc.py +156 -0
  28. glowcap/_gc.py +239 -0
  29. glowcap/_host/__init__.py +493 -0
  30. glowcap/_host/finalizer.py +176 -0
  31. glowcap/_host/lifecycle.py +391 -0
  32. glowcap/_host/progress.py +294 -0
  33. glowcap/_host/publication.py +347 -0
  34. glowcap/_host/state.py +114 -0
  35. glowcap/_host/vows.py +116 -0
  36. glowcap/_host_action_gen.py +133 -0
  37. glowcap/_iroh_async.py +486 -0
  38. glowcap/_iroh_ffi.py +484 -0
  39. glowcap/_layouts_gen.py +325 -0
  40. glowcap/_lib_resolve.py +391 -0
  41. glowcap/_lifecycle.py +75 -0
  42. glowcap/_listener.py +65 -0
  43. glowcap/_locator.py +343 -0
  44. glowcap/_loop/__init__.py +353 -0
  45. glowcap/_loop/configuration.py +40 -0
  46. glowcap/_loop/dialing.py +66 -0
  47. glowcap/_loop/errors.py +26 -0
  48. glowcap/_loop/lifecycle.py +248 -0
  49. glowcap/_loop/listeners.py +288 -0
  50. glowcap/_loop/progress.py +277 -0
  51. glowcap/_loop/sessions.py +151 -0
  52. glowcap/_loop/state.py +114 -0
  53. glowcap/_pin.py +82 -0
  54. glowcap/_promise.py +428 -0
  55. glowcap/_pump.py +303 -0
  56. glowcap/_server.py +136 -0
  57. glowcap/_session/__init__.py +301 -0
  58. glowcap/_session/lifecycle.py +410 -0
  59. glowcap/_session/profile.py +51 -0
  60. glowcap/_session/verbs.py +288 -0
  61. glowcap/_session/writer.py +152 -0
  62. glowcap/_sigs_gen.py +399 -0
  63. glowcap/_socket_address.py +10 -0
  64. glowcap/_tcp_listen.py +163 -0
  65. glowcap/_trace.py +361 -0
  66. glowcap/_trampoline.py +474 -0
  67. glowcap/_transport.py +102 -0
  68. glowcap/_value.py +156 -0
  69. glowcap/_ws.py +104 -0
  70. glowcap/c-abi-resolve.json +23 -0
  71. glowcap/inproc.py +248 -0
  72. glowcap/iroh.py +24 -0
  73. glowcap/onion.py +510 -0
  74. glowcap/py.typed +0 -0
  75. glowcap/relay.py +298 -0
  76. glowcap/tcp.py +443 -0
  77. glowcap/webrtc.py +540 -0
  78. glowcap-0.1.1.dist-info/METADATA +171 -0
  79. glowcap-0.1.1.dist-info/RECORD +82 -0
  80. glowcap-0.1.1.dist-info/WHEEL +5 -0
  81. glowcap-0.1.1.dist-info/licenses/LICENSE-APACHE +201 -0
  82. glowcap-0.1.1.dist-info/top_level.txt +1 -0
glowcap/__init__.py ADDED
@@ -0,0 +1,189 @@
1
+ """Idiomatic Python API for glowcap, an OCapN/CapTP Host driven by the
2
+ symbol-disjoint Host and driver archives (ADR-0024).
3
+
4
+ This is the core package: it rides ``libglowcap_ocapn`` (the shared value and
5
+ locator vocabulary), ``libglowcap_host`` (the byte-fed multi-session Host),
6
+ and ``libglowcap_driver`` (the stepping driver that adopts that Host). It does
7
+ not choose or load a concrete transport. The raw ``ctypes``
8
+ FFI lives in the internal ``_ffi_*`` modules. This package is the public
9
+ surface over it. What you hold is a :class:`Session`: compose a :class:`Runtime`,
10
+ dial with its ``connect`` method, accept through a transport module's
11
+ ``listen``, and speak plain Python values:
12
+ ``int``/``float``/``bool``/``bytes``/``str``/``list``, plus :class:`Symbol`,
13
+ :class:`Keyword`, :class:`Set`, :class:`Tagged`, :class:`Struct`, and the capability
14
+ :class:`Ref`:
15
+
16
+ async with Runtime.open(provider=tcp.provider()) as runtime:
17
+ async with await runtime.connect(tcp.locator("127.0.0.1", 9099)) as session:
18
+ greeter = await session.fetch("greeter")
19
+ reply = await session.send(greeter, "greet", ["world"])
20
+
21
+ The host owns routing, three-party redemption, and gift origination. The
22
+ binding moves bytes and honors the host's action queue. A :class:`Host` is the
23
+ CapTP Host those sessions share, one identity and one handoff router, read as
24
+ ``server.host`` or transferred into :class:`Runtime`.
25
+
26
+ The C ABI ships as a set of composable, symbol-disjoint archives (ADR-0022),
27
+ and this package mirrors that à la carte: the core here, plus one submodule
28
+ per transport. Each transport submodule loads only its own archive, so
29
+ importing one transport never loads another. Compose what you need:
30
+
31
+ from glowcap import Runtime, tcp
32
+ from glowcap.tcp import TcpNetlayer # libglowcap_net_transport_tcp
33
+ from glowcap.inproc import InprocNetlayer # libglowcap_net_transport_inproc
34
+ from glowcap.iroh import listen # libglowcap_net_transport_iroh
35
+
36
+ The async :class:`Runtime` / :class:`Session` surface here is transport-neutral.
37
+ Callers explicitly compose a transport's ``provider(config)``
38
+ or ``connector(...)`` with :class:`Runtime`. No locator-shape dispatch or byte
39
+ fallback occurs in this module. A ``tcp``, ``keyed-tcp``, ``noise-tcp``,
40
+ ``iroh``, or routed relay connection runs on the driver archive: one stepping
41
+ loop owns the provider, pumping, and session release, parked on the asyncio
42
+ reactor. A plain TCP application needs this core plus :mod:`glowcap.tcp`, which also
43
+ holds the blocking ``glowcap_tcp_*`` frame shuttle. The socket-free in-process
44
+ transport lives in :mod:`glowcap.inproc`, the encrypted-QUIC transport in
45
+ :mod:`glowcap.iroh`, and the WebRTC dial in :mod:`glowcap.webrtc`. The core never imports one of those modules based on locator shape.
46
+
47
+ The implementation is split by responsibility across ``_``-private submodules
48
+ (``_errors``, ``_value``, ``_abi_value``, ``_context``, ``_locator``,
49
+ ``_trampoline``, ``_host``, ``_action``, ``_promise``, ``_gc``, ``_dial``,
50
+ ``_loop``, ``_pump``, ``_session``) alongside the per-archive ``_ffi_*``
51
+ loaders. This module is the barrel that re-exports the public surface. Import
52
+ every name from :mod:`glowcap`: the submodule layout is internal.
53
+ """
54
+
55
+ from __future__ import annotations
56
+
57
+ from ._carrier_failure import CarrierFailure, CarrierPhase
58
+ from ._constants import (
59
+ FEATURE_BASELINE,
60
+ FEATURE_FULL,
61
+ FEATURE_HANDOFF,
62
+ FEATURE_MINIMUM,
63
+ FEATURE_MULTI_LOCATION,
64
+ FEATURE_PIPELINING,
65
+ FEATURE_STURDYREF,
66
+ FEATURE_UNTAG,
67
+ FEATURE_WIRE_DELTA,
68
+ HANDSHAKE_TIMEOUT_MS,
69
+ HINT_ALT_LOCATORS,
70
+ HINT_DIRECT_ADDRESSES,
71
+ HINT_VIA,
72
+ TRANSPORT_ID,
73
+ TRANSPORT_INPROC,
74
+ TRANSPORT_IROH,
75
+ TRANSPORT_KEYED_RELAY,
76
+ TRANSPORT_KEYED_TCP,
77
+ TRANSPORT_NOISE_RELAY,
78
+ TRANSPORT_NOISE_TCP,
79
+ TRANSPORT_ONION,
80
+ TRANSPORT_TCP,
81
+ TRANSPORT_WEBRTC,
82
+ DurableProvenance,
83
+ )
84
+ from ._context import Context
85
+ from ._dial import Runtime
86
+
87
+ # Re-export the public surface from the per-responsibility submodules, in
88
+ # dependency order (errors and values first, then the Host and the application
89
+ # surface). A consumer imports every name from :mod:`glowcap`.
90
+ from ._errors import AbortDetail, FetchFailed, GlowcapError, VowBroken, abi_version, last_error
91
+ from ._failure import FailureEvent, FailureLayer, FailureScope, take_runtime_failures
92
+ from ._host import Host
93
+ from ._lifecycle import PendingWork
94
+ from ._locator import (
95
+ Locator,
96
+ SturdyRef,
97
+ designator_host_port,
98
+ encode_peer_locator,
99
+ format_locator_uri,
100
+ format_peer_uri,
101
+ format_sturdyref,
102
+ parse_locator,
103
+ parse_sturdyref,
104
+ )
105
+ from ._promise import RemotePromise
106
+ from ._session import PartnerIdentity, SendRedeemingResult, Session, SessionProfile
107
+ from ._trace import annotate_session, enable_trace
108
+ from ._trampoline import Caretaker
109
+ from ._transport import Connector, Provider
110
+ from ._value import Argument, ErrorValue, Forward, Keyword, Ref, Set, Struct, Symbol, Tagged
111
+
112
+ __all__ = [
113
+ # Version and diagnostics
114
+ "abi_version",
115
+ "last_error",
116
+ "take_runtime_failures",
117
+ "AbortDetail",
118
+ "CarrierFailure",
119
+ "CarrierPhase",
120
+ # Exceptions
121
+ "GlowcapError",
122
+ "VowBroken",
123
+ "FetchFailed",
124
+ # Argument vocabulary
125
+ "Ref",
126
+ "Symbol",
127
+ "Keyword",
128
+ "ErrorValue",
129
+ "Set",
130
+ "Tagged",
131
+ "Struct",
132
+ "Forward",
133
+ "Argument",
134
+ # Allocation scope
135
+ "Context",
136
+ "FailureEvent",
137
+ "FailureLayer",
138
+ "FailureScope",
139
+ # Application surface
140
+ "Session",
141
+ "SessionProfile",
142
+ "SendRedeemingResult",
143
+ "DurableProvenance",
144
+ "PartnerIdentity",
145
+ "RemotePromise",
146
+ "Host",
147
+ "PendingWork",
148
+ "Caretaker",
149
+ "Runtime",
150
+ "Provider",
151
+ "Connector",
152
+ "annotate_session",
153
+ "enable_trace",
154
+ "HANDSHAKE_TIMEOUT_MS",
155
+ "FEATURE_MINIMUM",
156
+ "FEATURE_BASELINE",
157
+ "FEATURE_PIPELINING",
158
+ "FEATURE_HANDOFF",
159
+ "FEATURE_WIRE_DELTA",
160
+ "FEATURE_STURDYREF",
161
+ "FEATURE_UNTAG",
162
+ "FEATURE_MULTI_LOCATION",
163
+ "FEATURE_FULL",
164
+ # Locator grammar
165
+ "Locator",
166
+ "SturdyRef",
167
+ "parse_locator",
168
+ "parse_sturdyref",
169
+ "format_locator_uri",
170
+ "format_peer_uri",
171
+ "format_sturdyref",
172
+ "designator_host_port",
173
+ "encode_peer_locator",
174
+ # Transport symbols
175
+ "TRANSPORT_TCP",
176
+ "TRANSPORT_ID",
177
+ "TRANSPORT_KEYED_TCP",
178
+ "TRANSPORT_ONION",
179
+ "TRANSPORT_NOISE_TCP",
180
+ "TRANSPORT_NOISE_RELAY",
181
+ "TRANSPORT_INPROC",
182
+ "TRANSPORT_IROH",
183
+ "TRANSPORT_KEYED_RELAY",
184
+ "TRANSPORT_WEBRTC",
185
+ # Locator hint keys
186
+ "HINT_ALT_LOCATORS",
187
+ "HINT_DIRECT_ADDRESSES",
188
+ "HINT_VIA",
189
+ ]
glowcap/_abi_value.py ADDED
@@ -0,0 +1,308 @@
1
+ """Internal: the ctypes ABI value and option projection.
2
+
3
+ Marshals the value vocabulary to and from the C ABI ``_Argument`` / ``_Reference``
4
+ structs: ``_arg_to_c`` / ``_args_to_c`` / ``_ref_to_c`` on the way out
5
+ (retaining each borrowed byte owner on a caller-supplied ``keepalive`` list for
6
+ the duration of the synchronous C call), ``_arg_from_c`` / ``_ref_from_c`` on
7
+ the way back.
8
+ This module does not encode or decode CapTP frames. Zig owns both wire codecs
9
+ and every protocol decision. Re-exported value types come from :mod:`glowcap`.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import array
15
+ import asyncio
16
+ import ctypes
17
+ from collections.abc import Callable
18
+
19
+ from . import _constants as _abi
20
+ from ._errors import GlowcapError
21
+ from ._ffi_ocapn import _Argument, _borrowed_byte_ptr, _Reference
22
+ from ._value import Argument, ErrorValue, Keyword, Ref, Set, Struct, Symbol, Tagged, _ref_from_id
23
+
24
+ _ERR_INVALID_ARGUMENT = _abi.ERR_INVALID_ARGUMENT
25
+ _ERR_INTERNAL = _abi.ERR_INTERNAL
26
+ _ERR_UNSUPPORTED_VALUE = _abi.ERR_UNSUPPORTED_VALUE
27
+
28
+ _ARG_INTEGER, _ARG_FLOAT64, _ARG_BOOLEAN, _ARG_BYTES = (
29
+ _abi.ARG_INTEGER,
30
+ _abi.ARG_FLOAT64,
31
+ _abi.ARG_BOOLEAN,
32
+ _abi.ARG_BYTES,
33
+ )
34
+ _ARG_STRING, _ARG_SYMBOL, _ARG_VOID, _ARG_REFERENCE, _ARG_LIST = (
35
+ _abi.ARG_STRING,
36
+ _abi.ARG_SYMBOL,
37
+ _abi.ARG_VOID,
38
+ _abi.ARG_REFERENCE,
39
+ _abi.ARG_LIST,
40
+ )
41
+ _ARG_SET, _ARG_TAGGED, _ARG_KEYWORD = _abi.ARG_SET, _abi.ARG_TAGGED, _abi.ARG_KEYWORD
42
+ _ARG_BIG_INTEGER = _abi.ARG_BIG_INTEGER
43
+ _ARG_INTEGERS = _abi.ARG_INTEGERS
44
+ _ARG_STRUCT = _abi.ARG_STRUCT
45
+ _ARG_ERROR = _abi.ARG_ERROR
46
+ _FEATURE_FULL = _abi.FEATURE_FULL
47
+
48
+
49
+ def _codec_name(codec: int, source: str | None = None) -> str:
50
+ if codec == _abi.CODEC_SYRUP:
51
+ return "syrup"
52
+ if codec == _abi.CODEC_CAPNP:
53
+ return "capnp"
54
+ if source is not None:
55
+ raise RuntimeError(f"glowcap ABI returned unknown {source} codec {codec!r}")
56
+ raise RuntimeError(f"glowcap ABI generated unknown default codec {codec!r}")
57
+
58
+
59
+ def _captp_version_name(version: int, source: str | None = None) -> str:
60
+ if version == _abi.CAPTP_V1_0:
61
+ return "v1_0"
62
+ if version == _abi.CAPTP_NEXT:
63
+ return "next"
64
+ if source is not None:
65
+ raise RuntimeError(f"glowcap ABI returned unknown {source} CapTP version {version!r}")
66
+ raise RuntimeError(f"glowcap ABI generated unknown default CapTP version {version!r}")
67
+
68
+
69
+ def _feature_name(features: int):
70
+ if features == _abi.FEATURE_FULL:
71
+ return "full"
72
+ if features == _abi.FEATURE_BASELINE:
73
+ return "baseline"
74
+ if features == _abi.FEATURE_MINIMUM:
75
+ return "minimum"
76
+ return features
77
+
78
+
79
+ _DEFAULT_CODEC = _codec_name(_abi.DEFAULT_CODEC)
80
+ _DEFAULT_CAPTP_VERSION = _captp_version_name(_abi.DEFAULT_CAPTP_VERSION)
81
+ _DEFAULT_FEATURES = _feature_name(_abi.DEFAULT_FEATURES)
82
+ _INTEROP_CODEC = _codec_name(_abi.INTEROP_CODEC)
83
+ _INTEROP_CAPTP_VERSION = _captp_version_name(_abi.INTEROP_CAPTP_VERSION)
84
+ _OWN_CODEC = _codec_name(_abi.OWN_CODEC)
85
+ _OWN_CAPTP_VERSION = _captp_version_name(_abi.OWN_CAPTP_VERSION)
86
+
87
+
88
+ def _captp_version_id(version: str) -> int:
89
+ """The ``GLOWCAP_CAPTP_*`` id for a CapTP profile name (ADR-0001).
90
+ ``v1_0`` is the interop floor a Goblins vat speaks. ``next`` is the
91
+ glowcap-to-glowcap track ``Offer.glowcap`` prefers with Cap'n Proto."""
92
+ if version == "v1_0":
93
+ return _abi.CAPTP_V1_0
94
+ if version == "next":
95
+ return _abi.CAPTP_NEXT
96
+ raise ValueError(f'captp_version must be "v1_0" or "next", got {version!r}')
97
+
98
+
99
+ def _codec_id(codec: str) -> int:
100
+ """The ``GLOWCAP_CODEC_*`` id for a codec name, the selector
101
+ ``glowcap_host_new`` and the blocking transport connections take."""
102
+ if codec == "syrup":
103
+ return _abi.CODEC_SYRUP
104
+ if codec == "capnp":
105
+ return _abi.CODEC_CAPNP
106
+ raise ValueError(f"unknown OCapN codec {codec!r}")
107
+
108
+
109
+ def _feature_mask(features) -> int:
110
+ """The ``GLOWCAP_FEATURE_*`` bitmask for a Host feature offer."""
111
+ if features == "full":
112
+ return _abi.FEATURE_FULL
113
+ if features == "baseline":
114
+ return _abi.FEATURE_BASELINE
115
+ if features == "minimum":
116
+ return _abi.FEATURE_MINIMUM
117
+ if isinstance(features, int) and not isinstance(features, bool):
118
+ if features < 0 or features & ~_FEATURE_FULL:
119
+ raise ValueError(f"unknown OCapN feature mask {features!r}")
120
+ return features
121
+ mask = 0
122
+ for feature in features:
123
+ if feature == "pipelining":
124
+ mask |= _abi.FEATURE_PIPELINING
125
+ elif feature == "handoff":
126
+ mask |= _abi.FEATURE_HANDOFF
127
+ elif feature == "wire_delta":
128
+ mask |= _abi.FEATURE_WIRE_DELTA
129
+ elif feature == "sturdyref":
130
+ mask |= _abi.FEATURE_STURDYREF
131
+ elif feature == "untag":
132
+ mask |= _abi.FEATURE_UNTAG
133
+ elif feature == "multi_location":
134
+ mask |= _abi.FEATURE_MULTI_LOCATION
135
+ else:
136
+ raise ValueError(f"unknown OCapN feature {feature!r}")
137
+ return mask
138
+
139
+
140
+ def _ref_to_c(ref: Ref) -> _Reference:
141
+ return _Reference(ref._live().id)
142
+
143
+
144
+ # The capability a promise among send arguments travels as (ADR-0068). Only
145
+ # the far send and tell paths supply one; everywhere else a promise is an
146
+ # unsupported argument value.
147
+ _PromiseLowering = Callable[[asyncio.Future], Ref]
148
+
149
+
150
+ def _arg_to_c(value: Argument, keepalive: list, lower: _PromiseLowering | None = None) -> _Argument:
151
+ a = _Argument()
152
+ if isinstance(value, array.array) and value.typecode == "q":
153
+ # Export the buffer before reading its address: retaining the array
154
+ # alone permits another thread to resize it while CDLL releases the
155
+ # GIL. The export pins its storage for the call. Empty is NULL and 0.
156
+ a.kind = _ARG_INTEGERS
157
+ words = memoryview(value)
158
+ if words.nbytes:
159
+ keepalive.append(words)
160
+ a.bytes = value.buffer_info()[0]
161
+ a.bytes_len = words.nbytes
162
+ elif isinstance(value, bool): # before int; bool is an int subclass
163
+ a.kind, a.boolean = _ARG_BOOLEAN, 1 if value else 0
164
+ elif isinstance(value, int):
165
+ # ctypes silently wraps an out-of-range int mod 2**64; OCapN integers are
166
+ # bignums, so refuse anything the C ABI int64 cannot carry rather than
167
+ # send a corrupted value.
168
+ if not (-(2**63) <= value < 2**63):
169
+ # Outside the C ABI int64: ARG_BIG_INTEGER carries the canonical
170
+ # decimal digits in bytes/bytes_len and the sign in boolean (ADR-0005).
171
+ _set_bytes(a, _ARG_BIG_INTEGER, str(abs(value)).encode(), keepalive)
172
+ a.boolean = 1 if value < 0 else 0
173
+ else:
174
+ a.kind, a.integer = _ARG_INTEGER, value
175
+ elif isinstance(value, float):
176
+ a.kind, a.float64 = _ARG_FLOAT64, value
177
+ elif isinstance(value, Symbol): # before str
178
+ _set_bytes(a, _ARG_SYMBOL, value.encode(), keepalive)
179
+ elif isinstance(value, Keyword): # before str
180
+ _set_bytes(a, _ARG_KEYWORD, value.encode(), keepalive)
181
+ elif isinstance(value, ErrorValue):
182
+ _set_bytes(a, _ARG_ERROR, value.message.encode(), keepalive)
183
+ elif isinstance(value, str):
184
+ _set_bytes(a, _ARG_STRING, value.encode(), keepalive)
185
+ elif isinstance(value, (bytes, bytearray)):
186
+ _set_bytes(a, _ARG_BYTES, bytes(value), keepalive)
187
+ elif value is None:
188
+ a.kind = _ARG_VOID
189
+ elif isinstance(value, Ref):
190
+ a.kind, a.reference = _ARG_REFERENCE, _ref_to_c(value)
191
+ elif isinstance(value, asyncio.Future):
192
+ if lower is None:
193
+ raise GlowcapError(_ERR_UNSUPPORTED_VALUE, "a promise is passable only as a send or tell argument")
194
+ a.kind, a.reference = _ARG_REFERENCE, _ref_to_c(lower(value))
195
+ elif isinstance(value, Tagged):
196
+ _set_bytes(a, _ARG_TAGGED, value.tag.encode(), keepalive)
197
+ a.items, a.items_len = _args_to_c(list(value.value), keepalive, lower)
198
+ elif isinstance(value, Struct):
199
+ a.kind = _ARG_STRUCT
200
+ a.items, a.items_len = _struct_entries_to_c(value.entries, keepalive, lower)
201
+ elif isinstance(value, Set): # before list
202
+ a.kind = _ARG_SET
203
+ a.items, a.items_len = _args_to_c(list(value), keepalive, lower)
204
+ elif isinstance(value, list):
205
+ a.kind = _ARG_LIST
206
+ a.items, a.items_len = _args_to_c(value, keepalive, lower)
207
+ else:
208
+ raise GlowcapError(_ERR_UNSUPPORTED_VALUE, "unsupported argument value")
209
+ return a
210
+
211
+
212
+ def _set_bytes(a: _Argument, kind: int, data: bytes, keepalive: list) -> None:
213
+ owner, ptr = _borrowed_byte_ptr(data)
214
+ keepalive.append(owner)
215
+ a.kind = kind
216
+ a.bytes = ptr
217
+ a.bytes_len = len(data)
218
+
219
+
220
+ def _args_to_c(args: list, keepalive: list, lower: _PromiseLowering | None = None):
221
+ n = len(args)
222
+ if n == 0:
223
+ return (None, 0)
224
+ arr = (_Argument * n)()
225
+ for i, value in enumerate(args):
226
+ arr[i] = _arg_to_c(value, keepalive, lower)
227
+ keepalive.append(arr)
228
+ return (arr, n)
229
+
230
+
231
+ def _struct_entries_to_c(entries: list[tuple[str, Argument]], keepalive: list, lower: _PromiseLowering | None = None):
232
+ n = len(entries) * 2
233
+ if n == 0:
234
+ return (None, 0)
235
+ arr = (_Argument * n)()
236
+ for i, (key, value) in enumerate(entries):
237
+ if not isinstance(key, str) or isinstance(key, (Symbol, Keyword)):
238
+ raise GlowcapError(_ERR_UNSUPPORTED_VALUE, "Struct entries must be String/value pairs")
239
+ key_arg = _Argument()
240
+ _set_bytes(key_arg, _ARG_STRING, key.encode(), keepalive)
241
+ arr[i * 2] = key_arg
242
+ arr[i * 2 + 1] = _arg_to_c(value, keepalive, lower)
243
+ keepalive.append(arr)
244
+ return (arr, n)
245
+
246
+
247
+ def _ref_from_c(r: _Reference) -> Ref:
248
+ return _ref_from_id(r.id)
249
+
250
+
251
+ def _bytes_from_c(a: _Argument) -> bytes:
252
+ if not a.bytes or a.bytes_len == 0:
253
+ return b""
254
+ return ctypes.string_at(a.bytes, a.bytes_len)
255
+
256
+
257
+ def _arg_from_c(a: _Argument) -> Argument:
258
+ if a.kind == _ARG_INTEGER:
259
+ return a.integer
260
+ if a.kind == _ARG_FLOAT64:
261
+ return a.float64
262
+ if a.kind == _ARG_BOOLEAN:
263
+ return a.boolean != 0
264
+ if a.kind == _ARG_BYTES:
265
+ return _bytes_from_c(a)
266
+ # Syrup strings/symbols are unchecked partner bytes; decode leniently so a
267
+ # untrusted non-UTF-8 payload surfaces as a value (matching Rust's
268
+ # from_utf8_lossy and Deno's non-fatal TextDecoder) instead of crashing the
269
+ # poll with UnicodeDecodeError.
270
+ if a.kind == _ARG_STRING:
271
+ return _bytes_from_c(a).decode(errors="replace")
272
+ if a.kind == _ARG_SYMBOL:
273
+ return Symbol(_bytes_from_c(a).decode(errors="replace"))
274
+ if a.kind == _ARG_ERROR:
275
+ return ErrorValue(_bytes_from_c(a).decode(errors="replace"))
276
+ if a.kind == _ARG_REFERENCE:
277
+ return _ref_from_c(a.reference)
278
+ if a.kind == _ARG_LIST:
279
+ return [_arg_from_c(a.items[i]) for i in range(a.items_len)]
280
+ if a.kind == _ARG_SET:
281
+ return Set(_arg_from_c(a.items[i]) for i in range(a.items_len))
282
+ if a.kind == _ARG_TAGGED:
283
+ return Tagged(_bytes_from_c(a).decode(errors="replace"), [_arg_from_c(a.items[i]) for i in range(a.items_len)])
284
+ if a.kind == _ARG_KEYWORD:
285
+ return Keyword(_bytes_from_c(a).decode(errors="replace"))
286
+ if a.kind == _ARG_VOID:
287
+ return None
288
+ if a.kind == _ARG_BIG_INTEGER:
289
+ # Canonical decimal digits + sign flag back to a Python int (ADR-0005).
290
+ magnitude = int(_bytes_from_c(a))
291
+ return -magnitude if a.boolean != 0 else magnitude
292
+ if a.kind == _ARG_INTEGERS:
293
+ # Copied out because the C memory is released after projection.
294
+ run = array.array("q")
295
+ run.frombytes(_bytes_from_c(a))
296
+ return run
297
+ if a.kind == _ARG_STRUCT:
298
+ if a.items_len % 2:
299
+ raise GlowcapError(_ERR_INTERNAL, "glowcap ABI returned an odd Struct item count")
300
+ entries: list[tuple[str, Argument]] = []
301
+ for i in range(0, a.items_len, 2):
302
+ key_arg = a.items[i]
303
+ if key_arg.kind != _ARG_STRING:
304
+ raise GlowcapError(_ERR_INTERNAL, "glowcap ABI returned a non-String Struct key")
305
+ key = _bytes_from_c(key_arg).decode(errors="replace")
306
+ entries.append((key, _arg_from_c(a.items[i + 1])))
307
+ return Struct(entries)
308
+ raise GlowcapError(_ERR_INTERNAL, f"glowcap ABI returned unknown argument kind {a.kind}")
glowcap/_action.py ADDED
@@ -0,0 +1,145 @@
1
+ """Internal: host-action execute: the embedder-contract switch the CapTP
2
+ host drain runs.
3
+
4
+ :class:`~glowcap._host.Host` owns park/kick/vows and the Session registry.
5
+ Decode/types come from the generated ``_host_action_gen`` module
6
+ (``zig build gen-binding-host-action``). This module owns the shared C drain
7
+ (``glowcap_host_pump_drain``) with sync park-only hooks, then runs each parked
8
+ arm (dial, flush, close, gift-id entropy, settle, fail) after the C loop
9
+ returns: so async I/O never runs inside the C callback.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import ctypes
15
+ import secrets
16
+
17
+ from . import _constants as _abi
18
+ from ._errors import GlowcapError, _check_host, _vow_failure
19
+ from ._ffi_host import _host, _HostPumpArmFn, _HostPumpHooks
20
+ from ._ffi_ocapn import _byte_buf
21
+ from ._host_action_gen import (
22
+ CancelDial,
23
+ Close,
24
+ Dial,
25
+ Fail,
26
+ Flush,
27
+ HostAction,
28
+ NeedGiftId,
29
+ Resume,
30
+ Settle,
31
+ decode,
32
+ dispatch,
33
+ )
34
+
35
+ _OK = _abi.OK
36
+
37
+
38
+ def _answer_gift_id(draw, provide, failed) -> None:
39
+ """Complete entropy failure separately from FFI ticket refusal."""
40
+ try:
41
+ gift_id = draw()
42
+ except Exception:
43
+ failed()
44
+ return
45
+ provide(gift_id)
46
+
47
+
48
+ def _park_actions(host) -> tuple[list[HostAction], bool]:
49
+ """Run ``glowcap_host_pump_drain`` with park-only arms. Return decoded
50
+ actions in queue order. Views are copied inside ``decode`` before the
51
+ next pop frees them."""
52
+ parked: list[HostAction] = []
53
+ callback_failure: BaseException | None = None
54
+
55
+ @_HostPumpArmFn
56
+ def park(_ctx, _host_ptr, action, out_progressed) -> int:
57
+ nonlocal callback_failure
58
+ try:
59
+ parked.append(decode(action.contents))
60
+ except BaseException as error:
61
+ callback_failure = error
62
+ return _abi.ERR_INTERNAL
63
+ if out_progressed:
64
+ out_progressed[0] = 1
65
+ return _OK
66
+
67
+ # Keep the callback + hooks alive for the duration of the C call via
68
+ # locals (do not stash on Host private fields).
69
+ hooks = _HostPumpHooks(None, park, park, park, park, park)
70
+ acted = ctypes.c_uint32(0)
71
+ yielded = ctypes.c_uint32(0)
72
+ rc = _host.glowcap_host_pump_drain(host.raw, ctypes.byref(hooks), ctypes.byref(acted), ctypes.byref(yielded))
73
+ if callback_failure is not None:
74
+ raise GlowcapError(
75
+ _abi.ERR_INTERNAL, f"glowcap ABI returned an invalid host action: {callback_failure}"
76
+ ) from callback_failure
77
+ _check_host(rc)
78
+ return parked, yielded.value != 0
79
+
80
+
81
+ class _Handlers:
82
+ """The executor for popped host actions. Dumb by construction: every decision
83
+ was already made in the core. Each arm is pure I/O or marshalling. The
84
+ kind inventory lives in generated ``dispatch``, so a new kind fails
85
+ generate/check. One instance per Host: building this table per action
86
+ cost more than the action it dispatched."""
87
+
88
+ __slots__ = ("_host",)
89
+
90
+ def __init__(self, host) -> None:
91
+ self._host = host
92
+
93
+ async def dial(self, a: Dial) -> None:
94
+ # A popped DIAL is not owned by the FFI action queue. Hand
95
+ # it to the Host before returning so teardown can cancel and join it
96
+ # without serializing this drain behind network establishment.
97
+ from ._dial import run_dial_ticket
98
+
99
+ self._host.start_dial_task(
100
+ a.ticket, a.route_index, lambda: run_dial_ticket(self._host, a.ticket, a.route_index)
101
+ )
102
+
103
+ async def cancel_dial(self, a: CancelDial) -> None:
104
+ await self._host.cancel_dial_task(a.ticket, a.route_index)
105
+
106
+ async def flush(self, a: Flush) -> None:
107
+ # The session's writer owns the write, so this drain, and the serve
108
+ # loop running it, keeps reading while the partner is slow to. A write
109
+ # failure retires only that session, never this Host drain.
110
+ session = self._host.get_session(a.session_ptr)
111
+ if session is not None:
112
+ session.request_flush()
113
+
114
+ async def close(self, a: Close) -> None:
115
+ # The writer drains what the core queued ahead of this CLOSE, then
116
+ # retires the session and settles an explicit close.
117
+ session = self._host.get_session(a.session_ptr)
118
+ if session is not None:
119
+ session.close_after_flush(a.result)
120
+
121
+ async def need_gift_id(self, a: NeedGiftId) -> None:
122
+ def provide(gift_id: bytes) -> None:
123
+ buf, ptr = _byte_buf(gift_id)
124
+ _check_host(_host.glowcap_host_provide_gift_id(self._host.raw, a.ticket, ptr, 32))
125
+
126
+ _answer_gift_id(
127
+ lambda: secrets.token_bytes(32),
128
+ provide,
129
+ lambda: _check_host(_host.glowcap_host_gift_id_failed(self._host.raw, a.ticket)),
130
+ )
131
+
132
+ async def settle(self, a: Settle) -> None:
133
+ self._host.settle_vow(a.ticket, True, a.value)
134
+
135
+ async def fail(self, a: Fail) -> None:
136
+ self._host.settle_vow(a.ticket, False, _vow_failure(a.result, a.message))
137
+
138
+ async def resume(self, a: Resume) -> None:
139
+ # The shared C pump reports this as `yielded` before dispatch; keep
140
+ # the exhaustive generated handler complete for direct callers.
141
+ _ = a
142
+
143
+
144
+ async def _run_action(host, action: HostAction) -> None:
145
+ await dispatch(action, host.action_handlers)
@@ -0,0 +1,25 @@
1
+ """Pre-call compatibility check for separately loaded FFI archives."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ctypes
6
+ from collections.abc import Callable
7
+ from typing import Any
8
+
9
+
10
+ def require_archive_fingerprint(resolve: Callable[[], Any], stem: str, expected: int) -> None:
11
+ """Resolve and compare one archive fingerprint before ordinary use."""
12
+ try:
13
+ read = resolve()
14
+ except AttributeError as error:
15
+ raise RuntimeError(
16
+ f"{stem}: missing archive fingerprint; rebuild the FFI libraries from this checkout"
17
+ ) from error
18
+ read.argtypes = []
19
+ read.restype = ctypes.c_uint64
20
+ actual = int(read())
21
+ if actual != expected:
22
+ raise RuntimeError(
23
+ f"{stem}: callable surface does not match this binding "
24
+ f"(expected {expected:016x}, got {actual:016x}); rebuild the FFI libraries"
25
+ )