Documentation
¶
Index ¶
- func ExtractStdlib() (string, error)
- func Init() error
- func PerlClose(h uint64) error
- func PerlEval(h uint64, src string) (string, error)
- func PerlInterruptAddr(h uint64) (uint32, error)
- func PerlNew(libDir 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 Interpreter — are cheap. The returned directory is the value to pass as Config.StdlibDir (it becomes @INC via perl_new).
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 PerlClose ¶
Destroy the interpreter (perl_destruct + perl_free). PERL_SYS_TERM runs at process teardown, not here, so the handle is fully torn down but the process stays usable.
func PerlEval ¶
Evaluate `src` in the interpreter and return the result as a JSON string:
{"ok":
<bool >,"result": <string >,"stdout": <string >,"stderr": <string >,
"error":
<string >}
`src` is run via eval_pv (a string eval in scalar context). "result" holds the stringification of the returned scalar. "stdout"/"stderr" hold anything printed to STDOUT/STDERR during the eval (captured by reopening them onto in-memory scalars around the call). On a Perl-level die (or a host interrupt, see below), "ok" is false and "error" holds $ @ (the contents of ERRSV). Package/lexical-our state persists 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 PerlInterruptAddr ¶
---- Interruption support ------------------------------------------------
Lets a host watchdog goroutine abort a runaway eval (e.g. `while(1){}`) WITHOUT executing any wasm/C code on that instance — which would corrupt the shared linear-memory C stack. Perl gives us a cleaner hook than CPython's eval-breaker: the run loop is pluggable (PL_runops). perl_new installs a custom loop that, on every opcode, tests a host-writable flag word and, when set, calls Perl_croak("Perl execution interrupted"). The croak longjmps to the eval_pv trap, so perl_eval returns cleanly with ok=false and the message in "error" — exactly like a Perl-level die.
To interrupt, the host performs a single plain 32-bit store into linear memory (no call into the instance):
*(uint32_t *)perl_interrupt_addr(h) = 1;
The guest clears the flag when it fires. Addresses are 32-bit linear-memory offsets (wasm32). Like CPython's eval-breaker, this only fires at opcode boundaries: a single long-running op (a pathological regex, a big sort) is not preempted until it returns to the run loop.
func PerlNew ¶
Initialize a Perl interpreter and return an opaque handle (0 on failure). Call once per wasm instance.
Internally: PERL_SYS_INIT3 (once per process), perl_alloc, perl_construct, perl_parse with a minimal bootstrap (`-e 0`) so the interpreter is ready to eval, then installs the interruptible run loop (see below) by overwriting PL_runops. `lib_dir`, when non-empty, is prepended to so pure-Perl modules can be found on the runtime's WASI filesystem mount; pass NULL/empty to run with only the built-in .
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 Perl standard library (the
// assembled lib/ tree). It becomes @INC via perl_new. When empty, the
// embedded stdlib is extracted to a temp dir and used.
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 (%ENV). 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 open fails).
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. 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. When nil, all lookups are allowed.
Resolve func(host string) bool
// Stdin, when non-nil, backs the guest's fd 0 (STDIN). 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). Note: Perl-level print output is also captured into
// EvalResult.Stdout by the bridge; Stdout here is the raw fd sink.
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) 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. Reserving capacity up front
// makes boot-time grows zero-copy reslices, dropping a freshly-booted
// interpreter's resident memory. The reservation is virtual address space,
// not resident memory. When 0, a default headroom is used.
MemoryReserveBytes int
// Exec, when non-nil, is the subprocess whitelist. It is called before
// every process spawn with the executable path and full argv; returning
// false denies the spawn. 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 is routed to it, so
// giving two interpreters separate FS values isolates them completely. 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 interpreter: no host env is leaked and the embedded standard library is extracted and used automatically.
type EvalResult ¶
type EvalResult struct {
// Ok is false when the eval died (a Perl-level die/croak, a compile
// error, or a host interrupt); Error then holds $@.
Ok bool `json:"ok"`
// Result is the stringified value of the evaluated expression (the value
// of the last statement), valid only when Ok is true.
Result string `json:"result"`
// Stdout / Stderr capture what the eval printed to STDOUT / STDERR (the
// bridge redirects both onto in-memory scalars for the duration).
Stdout string `json:"stdout"`
Stderr string `json:"stderr"`
// Error is $@ when Ok is false.
Error string `json:"error"`
}
EvalResult is the decoded form of the JSON document perl_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 Perl interpreter.
func NewInterpreter ¶
func NewInterpreter(cfg Config) (inst *Interpreter, err error)
NewInterpreter builds a fresh wasm instance, applies the sandbox config, initializes the Perl 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 package (its main::) and returns the structured result. A Perl-level die 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 stops a running evaluation WITHOUT executing any wasm/C code on this instance. It writes the interrupt flag directly in linear memory; the interpreter's custom run loop (a pluggable PL_runops) checks the flag on every opcode and croaks at the next one, so Eval returns cleanly with Ok=false. Safe to call from another goroutine while Eval is running a long/infinite loop.
The flag address is fetched here via a normal call, 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) or pre-fetch it with PrepareInterrupt before starting the loop.
func (*Interpreter) PrepareInterrupt ¶
func (i *Interpreter) PrepareInterrupt() (*Interrupter, error)
PrepareInterrupt resolves the interrupt-flag address up front so Fire can run concurrently with a busy Eval (which holds the instance lock).
type Interrupter ¶
type Interrupter struct {
// contains filtered or unexported fields
}
Interrupter holds the pre-resolved interrupt-flag address. 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 trips the interrupt: it stores 1 into the interrupt-flag word. 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 flag lands in the array the guest observes. The run loop reads the word with a plain single-word load on its own goroutine, which is exactly the pluggable PL_runops contract.
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 Perl standard library at the root, ready to back an Interpreter:
fs := perl.NewStdlibMemFS()
interp, _ := perl.NewInterpreter(perl.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.