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.
- glowcap/__init__.py +189 -0
- glowcap/_abi_value.py +308 -0
- glowcap/_action.py +145 -0
- glowcap/_archive_contract.py +25 -0
- glowcap/_carrier_failure.py +69 -0
- glowcap/_constants.py +80 -0
- glowcap/_constants_gen.py +220 -0
- glowcap/_context.py +239 -0
- glowcap/_dial.py +399 -0
- glowcap/_driver/__init__.py +1 -0
- glowcap/_driver/finalizer.py +109 -0
- glowcap/_driver/release.py +73 -0
- glowcap/_driver/transport.py +39 -0
- glowcap/_driver_event.py +52 -0
- glowcap/_driver_failure.py +143 -0
- glowcap/_errors.py +189 -0
- glowcap/_failure.py +219 -0
- glowcap/_ffi.py +153 -0
- glowcap/_ffi_driver.py +138 -0
- glowcap/_ffi_host.py +217 -0
- glowcap/_ffi_inproc.py +39 -0
- glowcap/_ffi_iroh.py +90 -0
- glowcap/_ffi_ocapn.py +278 -0
- glowcap/_ffi_onion.py +67 -0
- glowcap/_ffi_relay.py +50 -0
- glowcap/_ffi_tcp.py +55 -0
- glowcap/_ffi_webrtc.py +156 -0
- glowcap/_gc.py +239 -0
- glowcap/_host/__init__.py +493 -0
- glowcap/_host/finalizer.py +176 -0
- glowcap/_host/lifecycle.py +391 -0
- glowcap/_host/progress.py +294 -0
- glowcap/_host/publication.py +347 -0
- glowcap/_host/state.py +114 -0
- glowcap/_host/vows.py +116 -0
- glowcap/_host_action_gen.py +133 -0
- glowcap/_iroh_async.py +486 -0
- glowcap/_iroh_ffi.py +484 -0
- glowcap/_layouts_gen.py +325 -0
- glowcap/_lib_resolve.py +391 -0
- glowcap/_lifecycle.py +75 -0
- glowcap/_listener.py +65 -0
- glowcap/_locator.py +343 -0
- glowcap/_loop/__init__.py +353 -0
- glowcap/_loop/configuration.py +40 -0
- glowcap/_loop/dialing.py +66 -0
- glowcap/_loop/errors.py +26 -0
- glowcap/_loop/lifecycle.py +248 -0
- glowcap/_loop/listeners.py +288 -0
- glowcap/_loop/progress.py +277 -0
- glowcap/_loop/sessions.py +151 -0
- glowcap/_loop/state.py +114 -0
- glowcap/_pin.py +82 -0
- glowcap/_promise.py +428 -0
- glowcap/_pump.py +303 -0
- glowcap/_server.py +136 -0
- glowcap/_session/__init__.py +301 -0
- glowcap/_session/lifecycle.py +410 -0
- glowcap/_session/profile.py +51 -0
- glowcap/_session/verbs.py +288 -0
- glowcap/_session/writer.py +152 -0
- glowcap/_sigs_gen.py +399 -0
- glowcap/_socket_address.py +10 -0
- glowcap/_tcp_listen.py +163 -0
- glowcap/_trace.py +361 -0
- glowcap/_trampoline.py +474 -0
- glowcap/_transport.py +102 -0
- glowcap/_value.py +156 -0
- glowcap/_ws.py +104 -0
- glowcap/c-abi-resolve.json +23 -0
- glowcap/inproc.py +248 -0
- glowcap/iroh.py +24 -0
- glowcap/onion.py +510 -0
- glowcap/py.typed +0 -0
- glowcap/relay.py +298 -0
- glowcap/tcp.py +443 -0
- glowcap/webrtc.py +540 -0
- glowcap-0.1.1.dist-info/METADATA +171 -0
- glowcap-0.1.1.dist-info/RECORD +82 -0
- glowcap-0.1.1.dist-info/WHEEL +5 -0
- glowcap-0.1.1.dist-info/licenses/LICENSE-APACHE +201 -0
- 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
|
+
)
|