Documentation
¶
Index ¶
- func ExtractStdlib() (string, error)
- func Init() error
- func PyAsyncExcAddr(h uint64) (uint32, error)
- func PyClose(h uint64) error
- func PyEval(h uint64, src string) (string, error)
- func PyEvalBreakerAddr(h uint64) (uint32, error)
- func PyKeyboardInterruptObj(h uint64) (uint32, error)
- func PyNew(stdlibDir string) (uint64, error)
- type CallbackHandler
- type Config
- type EvalResult
- type FS
- type File
- type Interpreter
- type Interrupter
- type MemFS
- type Module
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ExtractStdlib ¶
ExtractStdlib unpacks the embedded standard library into a temporary directory (once per process) and returns its path. The result is cached, so repeated calls — e.g. one per Instance — are cheap. The returned directory is the value to pass as Config.StdlibDir.
func Init ¶
func Init() error
Init initializes the global module. Must be called before any API use. Safe to call multiple times (uses sync.Once).
func PyAsyncExcAddr ¶
func PyEval ¶
Evaluate `src` and return the result as a JSON object string:
{"ok":
<bool >,"repr": <string >,"stdout": <string >,"stderr": <string >,
"error":
<string >}
If `src` parses as a single expression, "repr" holds repr() of its value (empty for statements / None). "stdout"/"stderr" hold anything written to sys.stdout/sys.stderr during execution. On an uncaught exception, "ok" is false and "error" holds the formatted traceback. Globals persist across calls on the same handle (REPL-like).
A single JSON string return is used because the bridge generator surfaces only one response value to Go; bundling the outputs keeps one round-trip and one atomic result. The Go wrapper unmarshals it.
func PyEvalBreakerAddr ¶
---- Interruption support ------------------------------------------------
Mirrors PyThreadState_SetAsyncExc() done as plain memory writes, so a host watchdog goroutine can raise KeyboardInterrupt in a running interpreter WITHOUT executing any wasm/C code on that instance (which would corrupt the shared linear-memory C stack). To interrupt, the host performs:
*(uint32_t *)py_async_exc_addr(h) = py_keyboard_interrupt_obj(h); atomic_or((uint32_t *)py_eval_breaker_addr(h), 8); // _PY_ASYNC_EXCEPTION_BIT
CPython checks eval_breaker on every bytecode backward edge (3.9+), so it raises KeyboardInterrupt at the next loop iteration — including a pure `while True: pass`. PyExc_KeyboardInterrupt is immortal in 3.14, so storing it needs no refcount bookkeeping. Addresses are 32-bit linear-memory offsets (wasm32). The async-exception bit is the constant 8 (1u < < 3).
func PyKeyboardInterruptObj ¶
func PyNew ¶
Initialize the CPython runtime (isolated config) and return an opaque interpreter handle (0 on failure). Call once per wasm instance.
`stdlib_dir` is the directory holding the Python standard library (the `Lib/` tree: encodings/, io.py, codecs.py, ...). It becomes the sole module search path. The isolated config ignores PYTHONPATH, so the host MUST pass this (the dir is reached through the runtime's WASI filesystem mount). Pass NULL/empty to fall back to CPython's default path discovery (usually fails in the sandbox — provide the path).
Types ¶
type CallbackHandler ¶
CallbackHandler is implemented by Go types that need to be called from C++. The type is always defined (the Module struct references it); the registration/dispatch machinery is only emitted when the wasm imports wasmify.callback_invoke.
type Config ¶
type Config struct {
// StdlibDir is the directory holding the Python standard library (the
// Lib/ tree). It becomes the sole module search path. Required for any
// non-trivial import (encodings is needed even for startup).
StdlibDir string
// PreopenDir scopes the guest filesystem root ("/") to this host
// directory. Empty leaves the host "/" visible (no scoping).
PreopenDir string
// Env is the environment the guest sees. nil means an empty
// environment — the host process os.Environ() is NOT leaked.
Env []string
// FSAccess, when non-nil, is a per-open/create/unlink whitelist. It
// receives the guest path (relative to the preopen) and whether the
// access is a write; returning false denies it (the guest sees a
// PermissionError / OSError EACCES).
FSAccess func(path string, write bool) bool
// NetAccess, when non-nil, gates the socket accept/recv/send surface.
// op is "accept"/"recv"/"send"; returning false denies it.
NetAccess func(op string) bool
// Dial, when non-nil, is the OUTBOUND-connection whitelist. It is called
// with ("tcp", dotted-quad IP, port) before each connect; returning false
// denies the connection (the guest sees a connect error). When nil, all
// outbound connections are allowed.
Dial func(network, ip string, port int) bool
// Resolve, when non-nil, is the name-resolution whitelist. It is called
// with the host being resolved before each lookup; returning false denies
// it (the guest sees a name-resolution error). This is where a hostname
// policy such as "block example.com" is enforced. When nil, all lookups
// are allowed.
Resolve func(host string) bool
// Stdin, when non-nil, backs the guest's fd 0 (sys.stdin / input()).
// Defaults to an empty stream (the host process stdin is NOT used).
Stdin io.Reader
// Stdout, when non-nil, receives the guest's fd 1 writes (os-level
// stdout, e.g. os.write(1, ...)). Note: Python print() output is also
// captured into EvalResult.Stdout by the bridge; Stdout here is the
// raw fd sink, used for streaming and by the CLI front-end.
Stdout io.Writer
// Stderr, when non-nil, receives the guest's fd 2 writes.
Stderr io.Writer
// MaxMemoryBytes, when > 0, caps this interpreter's wasm linear memory.
// A guest allocation that would grow memory past this limit fails
// (memory.grow returns -1 -> the C allocator sees ENOMEM -> Python raises
// MemoryError) instead of growing the host process unbounded. Rounded
// down to a multiple of the 64 KiB wasm page size; values below the
// module's initial memory are ignored.
MaxMemoryBytes int
// MemoryReserveBytes, when > 0, is the initial linear-memory slice
// capacity reserved for this interpreter. CPython's wasm linear memory
// grows during boot; if the initial slice has no spare capacity the first
// grow reallocates and copies the whole linear memory, which makes the
// (otherwise untouched, zero) C-stack region resident and inflates RSS.
// Reserving capacity up front makes those grows zero-copy reslices, so a
// freshly-booted interpreter's resident memory drops dramatically. The
// reservation is virtual address space, not resident memory — untouched
// pages stay non-resident — so a generous value is cheap. It is clamped up
// to the module's minimum memory size. When 0, a default headroom is used
// (NewWithWASI), which already covers a normal boot.
MemoryReserveBytes int
// Exec, when non-nil, is the subprocess whitelist. It is called before
// every process spawn (subprocess.run/Popen via posix_spawn) with the
// executable path and full argv; returning false denies the spawn (the
// guest sees a PermissionError). Spawning runs a HOST binary, so a sandbox
// that allows subprocess should set this to a strict allow-list. When nil,
// all spawns are permitted (subject to host-subprocess being built in).
Exec func(path string, argv []string) bool
// FS, when non-nil, is the filesystem backend this interpreter sees as its
// entire guest filesystem ("/"). Every file operation (open/read/write/
// stat/mkdir/readdir/...) is routed to it, so giving two interpreters
// separate FS values isolates them completely — one cannot see another's
// files. Use NewStdlibMemFS() for an in-memory FS pre-loaded with the
// standard library. When FS is set, PreopenDir is ignored and StdlibDir
// defaults to "/" (the FS root). When nil, the default os-backed filesystem
// (optionally scoped by PreopenDir) is used.
FS FS
}
Config configures a new Interpreter's sandbox. The zero value is a usable (but unsandboxed-filesystem) interpreter: no host env is leaked, but the preopen defaults to the host "/" until PreopenDir is set.
type EvalResult ¶
type EvalResult struct {
Ok bool `json:"ok"`
Repr string `json:"repr"`
Stdout string `json:"stdout"`
Stderr string `json:"stderr"`
Error string `json:"error"`
}
EvalResult is the decoded form of the JSON document py_eval returns.
type FS ¶
FS is the read/write filesystem backend an Interpreter is given via Config.FS. It is a write-capable superset of io/fs.FS.
type Interpreter ¶
type Interpreter struct {
// contains filtered or unexported fields
}
Interpreter is one isolated CPython interpreter.
func NewInterpreter ¶
func NewInterpreter(cfg Config) (inst *Interpreter, err error)
NewInterpreter builds a fresh wasm instance, applies the sandbox config, initializes the CPython runtime, and returns a ready interpreter.
func (*Interpreter) Close ¶
func (i *Interpreter) Close() error
Close finalizes the interpreter. The Interpreter must not be used afterward.
func (*Interpreter) Eval ¶
func (i *Interpreter) Eval(src string) (EvalResult, error)
Eval compiles and runs src in this interpreter's persistent globals and returns the structured result. A Python-level exception is reported via EvalResult.Ok=false / .Error, not as a Go error; a Go error indicates a host/transport failure (a wasm trap, encoding problem, ...).
func (*Interpreter) Interrupt ¶
func (i *Interpreter) Interrupt() error
Interrupt raises KeyboardInterrupt in a running evaluation WITHOUT executing any wasm/C code on this instance. It writes the async-exception object and sets the eval-breaker bit directly in linear memory, mirroring PyThreadState_SetAsyncExc. Safe to call from another goroutine while Eval is running a long/infinite loop — CPython checks the eval-breaker on every bytecode back-edge and raises at the next iteration.
The three addresses are fetched here via normal calls, so callers that want to interrupt a loop must either call Interrupt from a separate goroutine (this call cannot acquire the per-instance lock while Eval holds it — see PrepareInterrupt) or pre-fetch them with PrepareInterrupt before starting the loop.
func (*Interpreter) PrepareInterrupt ¶
func (i *Interpreter) PrepareInterrupt() (*Interrupter, error)
PrepareInterrupt resolves the interrupt addresses up front and STAGES the async-exception object: the KeyboardInterrupt class pointer is written into the thread's async-exc slot here, before the loop starts. From then on the value is linear-memory CONTENT — every memory.grow (including ones that relocate the backing array) copies it along — so Fire only has to flip the eval-breaker bit, and the object is globally visible long before the bit can be (no cross-CPU store-ordering concern on arm64).
type Interrupter ¶
type Interrupter struct {
// contains filtered or unexported fields
}
Interrupter holds the pre-resolved state needed to raise KeyboardInterrupt. Resolve it with PrepareInterrupt BEFORE starting the loop you intend to interrupt (resolving needs the instance lock, which a running Eval holds), then call Fire from a watchdog goroutine.
func (*Interrupter) Fire ¶
func (ip *Interrupter) Fire()
Fire raises the interrupt: it sets _PY_ASYNC_EXCEPTION_BIT in the eval-breaker word (the exception object itself was already staged by PrepareInterrupt). It does not take the instance lock — that is the point: it runs concurrently with a busy Eval.
The write happens inside base.AccessMemory, which holds the same lock the runtime's memory.grow takes to mutate the memory slice header or relocate its backing array. That makes delivery deterministic: for the duration of the write the memory can neither be resliced nor relocated, so the bit lands in the array the guest observes — no snapshots, no retries, no polling. The only wait is bounded by an in-flight grow.
The breaker word itself is read-modify-written by the guest on its own goroutine with plain single-word accesses — that unsynchronised word is the eval-breaker protocol CPython defines, and the guest re-asserts its own bits on every pass, so the |= below is exactly as strong as CPython's native cross-thread signalling.
type MemFS ¶
MemFS is an in-memory read/write FS. Separate MemFS values are fully isolated from one another.
func NewStdlibMemFS ¶
NewStdlibMemFS returns an in-memory filesystem pre-loaded with the embedded Python standard library at the root, ready to back an Interpreter:
fs := python.NewStdlibMemFS()
interp, _ := python.NewInterpreter(python.Config{FS: fs}) // StdlibDir = "/"
Each call returns an independent FS, so interpreters built from separate NewStdlibMemFS() values share no filesystem state.
type Module ¶
type Module struct {
// contains filtered or unexported fields
}
Module is the bridge handle. It wraps the wasm2go-transpiled module (*base.Module) and serialises every entry on m.mu.
Reentrant callbacks — a host-import callback whose handler needs to make further calls back into the transpiled module — are supported by releasing m.mu around the user handler in handleCallback.
Safety rests on a single structural property: every path that touches transpiled module state goes through m.invoke, and m.invoke holds m.mu for the entire duration of its call. The release window in handleCallback simply lets another m.invoke grab the mutex and run as a fully nested top-level call — same goroutine via the user handler, different goroutine via an unrelated caller, it doesn't matter. Either way the nested call enters, balances its own state changes, and exits before the outer call resumes, so the outer call never observes a mid-flight inner call.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package cli implements a python-compatible command front-end on top of the go-python interpreter.
|
Package cli implements a python-compatible command front-end on top of the go-python interpreter. |
|
cmd
|
|
|
python
command
Command python is a thin wrapper around the cli package: a standalone `python` executable backed by the go-python (CPython-via-wasm2go) interpreter.
|
Command python is a thin wrapper around the cli package: a standalone `python` executable backed by the go-python (CPython-via-wasm2go) interpreter. |