polyplug-guest 0.1.0__tar.gz
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.
- polyplug_guest-0.1.0/PKG-INFO +21 -0
- polyplug_guest-0.1.0/polyplug_guest/__init__.py +222 -0
- polyplug_guest-0.1.0/polyplug_guest.egg-info/PKG-INFO +21 -0
- polyplug_guest-0.1.0/polyplug_guest.egg-info/SOURCES.txt +7 -0
- polyplug_guest-0.1.0/polyplug_guest.egg-info/dependency_links.txt +1 -0
- polyplug_guest-0.1.0/polyplug_guest.egg-info/requires.txt +1 -0
- polyplug_guest-0.1.0/polyplug_guest.egg-info/top_level.txt +1 -0
- polyplug_guest-0.1.0/pyproject.toml +35 -0
- polyplug_guest-0.1.0/setup.cfg +4 -0
|
@@ -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,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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
polyplug-abi
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
polyplug_guest
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "polyplug-guest"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Guest library for writing polyplug plugins in Python"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
keywords = ["polyplug", "plugin", "guest", "abi"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.10",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Typing :: Typed",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"polyplug-abi",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://github.com/polyplug/polyplug"
|
|
29
|
+
Documentation = "https://github.com/polyplug/polyplug#readme"
|
|
30
|
+
Repository = "https://github.com/polyplug/polyplug.git"
|
|
31
|
+
Issues = "https://github.com/polyplug/polyplug/issues"
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.packages.find]
|
|
34
|
+
where = ["."]
|
|
35
|
+
include = ["polyplug_guest*"]
|