polyplug-guest 0.1.0__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.
@@ -0,0 +1,222 @@
1
+ """polyplug_guest — guest-side Python library for polyplug plugin authors.
2
+
3
+ Python plugins are VM-dispatch plugins (like Lua and JavaScript): the guest
4
+ never builds a ``GuestContractInterface`` or registers native function
5
+ pointers. Instead the loader executes the plugin module and calls its
6
+ ``polyplug_init(host_ptr: int, ctx_ptr: int) -> tuple[list[dict], AbiError]``.
7
+ ``polyplug_init`` RETURNS its registrations directly — nothing is deposited into
8
+ any module namespace. The loader reads the returned tuple: ``abi_error.code ==
9
+ AbiErrorCode.Ok`` selects the registration list it then wraps in a VM-dispatch
10
+ interface and registers with the runtime itself; a non-Ok code surfaces as a
11
+ loader error.
12
+
13
+ This library provides the registration helper that appends to that list, the
14
+ ``StringView`` <-> ``str`` codecs, and the two cross-boundary allocators
15
+ (host-allocator for data that must outlive the call, arena for per-call return
16
+ buffers). It also re-exports the ABI types plugin authors need.
17
+
18
+ Per-call state (args, out, arena, arena_alloc) is passed explicitly through each
19
+ dispatch call. The ``HostApi`` pointer is NOT stored in this package: it flows
20
+ from ``polyplug_init`` into the author factory (``polyplug_create_<plugin>``),
21
+ which constructs the implementation with its owning runtime's host pointer.
22
+ Helpers that need the host (:func:`alloc_string`, :func:`log`, the generated
23
+ peer callers' ``resolve(host_ptr)``) take it as an explicit argument. The arena
24
+ allocator the guest forwards to :func:`alloc_string_arena` is likewise NOT a
25
+ module global: the loader passes it as the FINAL positional argument of every
26
+ dispatch call, and the generated glue threads it through.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import ctypes
32
+ from typing import Callable, List, Optional
33
+
34
+ from polyplug_abi import (
35
+ AbiErrorCode,
36
+ AbiError,
37
+ Buffer,
38
+ PluginDescriptor,
39
+ GuestContractHandle,
40
+ BundleInitContext,
41
+ HostApi,
42
+ LogLevel,
43
+ StringView,
44
+ DispatchType,
45
+ bytes_as_view,
46
+ to_str,
47
+ )
48
+
49
+ __all__ = [
50
+ "AbiErrorCode",
51
+ "AbiError",
52
+ "Buffer",
53
+ "PluginDescriptor",
54
+ "GuestContractHandle",
55
+ "BundleInitContext",
56
+ "HostApi",
57
+ "LogLevel",
58
+ "StringView",
59
+ "DispatchType",
60
+ "to_str",
61
+ "register_contract",
62
+ "alloc_string",
63
+ "alloc_string_arena",
64
+ "log",
65
+ ]
66
+
67
+ def log(host_ptr: int, level: int, scope: str, message: str) -> None:
68
+ """Send a guest diagnostic to the host's logging funnel (``HostApi.log``).
69
+
70
+ Routes to the same sink as ``RuntimeConfig::log``: the host-installed
71
+ callback when one is set, otherwise the host's stderr default (Error/Warn
72
+ visibility only). The host delivers ``(level, scope, message)`` verbatim and
73
+ copies what it needs before returning — nothing here outlives the call.
74
+
75
+ No-op when ``host_ptr`` is 0, so plugins may call this unconditionally.
76
+
77
+ Args:
78
+ host_ptr: the ``HostApi`` pointer handed to the author factory
79
+ (``polyplug_create_<plugin>``) — no host pointer is stored in this
80
+ package.
81
+ level: a :class:`polyplug_abi.LogLevel` value (``Error = 1`` ..
82
+ ``Trace = 5``); the host clamps unknown values to ``Error``.
83
+ scope: short stable tag — use ``"guest.<plugin-name>"`` by convention.
84
+ message: the log message.
85
+ """
86
+ if not host_ptr:
87
+ return
88
+ # ctypes does NOT root Python objects through a StringView's raw `ptr`
89
+ # field, so the encoded bytes objects must be kept alive explicitly. These
90
+ # two locals are the owners: they live until this function returns, which
91
+ # outlives the synchronous host.log call below. (The classic footgun is
92
+ # building the view from a temporary — `bytes_as_view(s.encode("utf-8"))`
93
+ # inline — where the bytes are collected before the call.)
94
+ scope_bytes: bytes = scope.encode("utf-8")
95
+ message_bytes: bytes = message.encode("utf-8")
96
+ scope_view: StringView = bytes_as_view(scope_bytes)
97
+ message_view: StringView = bytes_as_view(message_bytes)
98
+ host: HostApi = HostApi.from_address(host_ptr)
99
+ # Self-passing convention: log(this, level, scope, message). The host reads
100
+ # both views only for the duration of the call; null/empty views are legal.
101
+ host.log(host_ptr, int(level), scope_view, message_view)
102
+
103
+
104
+ def register_contract(
105
+ registrations: List[dict],
106
+ contract: str,
107
+ functions: List[Callable[[object, int, int, int, Callable[[int, int], int]], None]],
108
+ factory: Callable[[int], object],
109
+ plugin_name: Optional[str] = None,
110
+ ) -> None:
111
+ """Append one contract's registration to ``polyplug_init``'s return list.
112
+
113
+ Appends a registration dict — in the exact shape the loader expects — to the
114
+ ``registrations`` list ``polyplug_init`` returns to the loader. Nothing is
115
+ deposited into any module namespace: the loader reads the value
116
+ ``polyplug_init`` returns, not a module attribute. Call this from
117
+ ``polyplug_init`` once per contract the bundle provides, passing the local
118
+ list ``polyplug_init`` will return.
119
+
120
+ The loader owns per-instance state: it calls ``factory(host_ptr)`` once per
121
+ ``create_instance`` to build a fresh implementation object, keys it under the
122
+ returned instance handle, and threads it back as the first argument of every
123
+ dispatch callable. No implementation object is stored at module scope, so two
124
+ live instances of the same contract never share state.
125
+
126
+ Args:
127
+ registrations: the list ``polyplug_init`` will return to the loader; this
128
+ entry is appended to it.
129
+ contract: canonical contract string ``"<name>@<major>"`` or
130
+ ``"<name>@<major>.<minor>"`` (minor is parsed but does not affect
131
+ the contract id).
132
+ functions: callables ordered by ``fn_id`` — ``functions[0]`` is fn_id 0,
133
+ etc. Each is invoked as ``fn(impl, args_ptr_int: int, out_ptr_int:
134
+ int, arena_ptr_int: int, arena_alloc: Callable[[int, int], int])``
135
+ where ``impl`` is the instance the loader resolved for this call and
136
+ ``arena_alloc`` is the loader-supplied arena allocator (forward it to
137
+ :func:`alloc_string_arena`); return normally on success, raise to
138
+ signal an error.
139
+ factory: the author factory ``factory(host_ptr_int: int) -> impl`` the
140
+ loader calls once per ``create_instance`` (and once at load for the
141
+ stateless default instance) to build a fresh implementation bound to
142
+ its owning runtime's host pointer.
143
+ plugin_name: optional human-readable plugin name; the loader defaults to
144
+ the bundle name when omitted.
145
+ """
146
+ entry: dict = {
147
+ "contract": contract,
148
+ "functions": list(functions),
149
+ "factory": factory,
150
+ }
151
+ if plugin_name is not None:
152
+ entry["plugin_name"] = plugin_name
153
+ registrations.append(entry)
154
+
155
+
156
+ def alloc_string(host_ptr: int, s: str) -> StringView:
157
+ """Allocate a ``StringView`` in HOST memory from a Python string.
158
+
159
+ Use this for strings that must OUTLIVE the current call (e.g. data handed to
160
+ a host contract). Cross-boundary data must use the host allocator, so the
161
+ returned bytes live until the host frees them — the guest never frees them.
162
+ For per-call return values, prefer :func:`alloc_string_arena`.
163
+
164
+ Args:
165
+ host_ptr: the ``HostApi`` pointer the loader passed to ``polyplug_init``.
166
+ s: the Python string to allocate.
167
+
168
+ Returns:
169
+ a ``StringView`` pointing at the host-allocated UTF-8 bytes.
170
+ """
171
+ encoded: bytes = s.encode("utf-8")
172
+ if not encoded:
173
+ return StringView(ptr=None, len=0)
174
+ host: HostApi = HostApi.from_address(host_ptr)
175
+ # The host allocator uses the self-passing convention: alloc(this, size, align).
176
+ # `host.alloc` is the HostApi.alloc CFUNCTYPE field, so it takes the host
177
+ # interface pointer as its first argument; align 1 is valid for byte buffers.
178
+ ptr: int = host.alloc(host_ptr, len(encoded), 1)
179
+ if not ptr:
180
+ raise MemoryError("alloc_string: host allocation failed")
181
+ ctypes.memmove(ptr, encoded, len(encoded))
182
+ return StringView(ptr=ptr, len=len(encoded))
183
+
184
+
185
+ def alloc_string_arena(
186
+ arena_alloc: Callable[[int, int], int], arena_ptr: int, s: str
187
+ ) -> StringView:
188
+ """Allocate a per-call return ``StringView`` from THIS call's CallArena.
189
+
190
+ Use this for strings RETURNED from a contract function: the bytes are served
191
+ from the host's per-call arena and stay valid until the caller's next
192
+ arena-backed call, so the guest never frees them. For data that must outlive
193
+ the call, use :func:`alloc_string` instead.
194
+
195
+ The loader passes the arena allocator ``arena_alloc(size: int, arena: int) ->
196
+ int`` as the FINAL positional argument of every dispatch call (nothing is
197
+ injected into the plugin module). The arena pointer is NOT read from any
198
+ shared state: it is the ``arena`` int the dispatch passed to the guest
199
+ callable as its third argument, forwarded here as ``arena_ptr`` and on to
200
+ ``arena_alloc``. The allocator bumps exactly that arena (or falls back to
201
+ ``host->alloc`` when ``arena_ptr`` is 0). Threading both the arena and its
202
+ allocator explicitly — rather than through a shared cell or module global —
203
+ is what makes concurrent and same-thread reentrant dispatch correct: each
204
+ call's arena travels with its own call frame.
205
+
206
+ Args:
207
+ arena_alloc: the loader-supplied arena allocator this call received as its
208
+ final dispatch argument.
209
+ arena_ptr: the ``arena`` int this call received as its third argument.
210
+ s: the Python string to allocate.
211
+
212
+ Returns:
213
+ a ``StringView`` pointing at the arena-allocated UTF-8 bytes.
214
+ """
215
+ encoded: bytes = s.encode("utf-8")
216
+ if not encoded:
217
+ return StringView(ptr=None, len=0)
218
+ addr: int = arena_alloc(len(encoded), arena_ptr)
219
+ if not addr:
220
+ raise MemoryError("alloc_string_arena: arena allocation failed")
221
+ ctypes.memmove(addr, encoded, len(encoded))
222
+ return StringView(ptr=addr, len=len(encoded))
@@ -0,0 +1,21 @@
1
+ Metadata-Version: 2.4
2
+ Name: polyplug-guest
3
+ Version: 0.1.0
4
+ Summary: Guest library for writing polyplug plugins in Python
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/polyplug/polyplug
7
+ Project-URL: Documentation, https://github.com/polyplug/polyplug#readme
8
+ Project-URL: Repository, https://github.com/polyplug/polyplug.git
9
+ Project-URL: Issues, https://github.com/polyplug/polyplug/issues
10
+ Keywords: polyplug,plugin,guest,abi
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: polyplug-abi
@@ -0,0 +1,5 @@
1
+ polyplug_guest/__init__.py,sha256=iNGPADayUn3PPmYCTOs-tvNaoRzPbuczmmtECZAnN68,10104
2
+ polyplug_guest-0.1.0.dist-info/METADATA,sha256=5y8cNkH6kLER0jsa7ZRkgslmCwrWqR2bmqhuIMEGhb0,903
3
+ polyplug_guest-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
4
+ polyplug_guest-0.1.0.dist-info/top_level.txt,sha256=pnp8hneuW7BCy6McGDSioRxzgcyZgf9WK1jA8xeD6dY,15
5
+ polyplug_guest-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ polyplug_guest