Documentation
¶
Overview ¶
Example ¶
Compile once, call many times. A Program holds the interpreter as it stood after the source ran, so every call starts from that point rather than re-running the module.
package main
import (
"context"
"fmt"
"log"
micropython "github.com/gregfurman/micropython-go"
)
func main() {
ctx := context.Background()
p, err := micropython.CompileSource(ctx, `
def score(row):
total = row["a"] * 2 + row["b"]
return {"id": row["id"], "score": total, "ok": total > 10}
`)
if err != nil {
log.Fatal(err)
}
defer p.Close()
row := micropython.Of(map[string]any{"id": "r-1", "a": 4, "b": 5})
got, err := p.Call(ctx, "score", row)
if err != nil {
log.Fatal(err)
}
fmt.Println(got)
}
Output: map[id:r-1 ok:true score:13]
Index ¶
- Variables
- func Raise(typ, msg string) error
- type Globals
- type HostFunc
- type Instance
- func (i *Instance) Call(ctx context.Context, name string, args ...any) (any, error)
- func (i *Instance) Cancel() error
- func (i *Instance) Clone(ctx context.Context) (*Instance, error)
- func (i *Instance) Close() error
- func (i *Instance) DefineFunction(ctx context.Context, name string, fn HostFunc) error
- func (i *Instance) Err() error
- func (i *Instance) Eval(ctx context.Context, expr string) (any, error)
- func (i *Instance) Exec(ctx context.Context, src string) (string, error)
- func (i *Instance) Set(ctx context.Context, name string, v Value) error
- type Item
- type Option
- type Program
- type ProgramOption
- type PythonError
- type Value
- func Bool(b bool) Value
- func Bytes(b []byte) Value
- func Dict(entries ...Item) Value
- func Exception(typ, msg string) Value
- func Float(f float64) Value
- func FrozenSet(items ...Value) Value
- func Int(n int64) Value
- func Ints(items ...int64) Value
- func List(items ...Value) Value
- func None() Value
- func Of(v any) Value
- func Set(items ...Value) Value
- func Str(s string) Value
- func Strs(items ...string) Value
- func Tuple(items ...Value) Value
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrClosed indicates the interpreter or pool has been shut down and can no longer be used. ErrClosed = api.ErrClosed // ErrInterrupted indicates the guest execution was halted, either by context cancellation or a manual Cancel call. ErrInterrupted = api.ErrInterrupted // ErrInstanceNotInitialised is returned if an operation is attempted on an improperly constructed Instance. ErrInstanceNotInitialised = errors.New("cannot perform operation on Instance that has not been initialised") )
Functions ¶
func Raise ¶
Raise returns an error that makes the guest raise a specific Python exception. Return it from a HostFunc to control which class the caller sees:
in.DefineFunction(ctx, "lookup", func(args []any) (any, error) {
return nil, micropython.Raise("KeyError", "missing")
})
Python then catches it as a KeyError. A typ that does not name a builtin exception falls back to HostError, as does any other error a HostFunc returns, carrying that error's text as the message.
Types ¶
type Globals ¶
Globals is the set of names a program starts with, and their values.
p, err := micropython.Compile(ctx, src, micropython.WithGlobals(micropython.Globals{
"NAME": micropython.Str("service"),
"LIMITS": micropython.Dict(micropython.Str("retries"), micropython.Int(3)),
}))
The values are the built ones in value.go and nothing else, so a Go type with no Python equivalent is a compile error rather than an encoding one.
type Instance ¶
type Instance struct {
// contains filtered or unexported fields
}
Instance represents a single, stateful MicroPython interpreter.
Unlike a Program, an Instance maintains state between calls. Variables, imports, and function definitions created during one execution will persist and remain available for subsequent calls.
An Instance is safe for concurrent use across multiple goroutines, but it is strictly sequential. Because it operates on a single linear WebAssembly memory, concurrent calls will queue and execute one at a time. If you need parallel execution, use a Program or Clone this instance.
Example ¶
An Instance is one interpreter that keeps what it is told, for a session rather than a handler. Unlike a Program, state carries between calls.
package main
import (
"context"
"fmt"
"log"
micropython "github.com/gregfurman/micropython-go"
)
func main() {
ctx := context.Background()
in, err := micropython.NewInstance(ctx)
if err != nil {
log.Fatal(err)
}
defer in.Close()
if _, err := in.Exec(ctx, `
total = 0
def add(n):
global total
total += n
return total
`); err != nil {
log.Fatal(err)
}
var running []any
for _, n := range []int64{1, 2, 3} {
got, err := in.Call(ctx, "add", micropython.Int(n))
if err != nil {
log.Fatal(err)
}
running = append(running, got)
}
fmt.Println(running...)
// Eval reads one expression back.
got, err := in.Eval(ctx, "total * 10")
if err != nil {
log.Fatal(err)
}
fmt.Println(got)
}
Output: 1 3 6 60
func NewInstance ¶
NewInstance boots a fresh MicroPython interpreter.
If any globals are provided via options, they are injected into the Python environment immediately upon startup.
func (*Instance) Call ¶
Call invokes a Python global function by name, passing the provided arguments, and returns its result translated into a native Go value.
Because the Instance is stateful, the Python function may interact with or mutate globals that persist after the Call returns.
func (*Instance) Cancel ¶
Cancel interrupts any Python execution currently in flight on this instance. The running code receives a KeyboardInterrupt.
Safe from any goroutine. Cancelling an idle interpreter is also safe but does nothing: the request is cleared when the next call begins, so it cannot make a later call fail.
It is best effort. The request lands at the next VM hook, so a guest inside one long C-level operation -- a regex match, a big-int multiply, a sort -- does not stop until that finishes.
func (*Instance) Clone ¶
Clone captures a snapshot of the interpreter's current memory and returns a completely independent Instance starting from that exact state.
Cloning momentarily locks the underlying interpreter while the memory is copied, meaning no other calls can execute until the clone is complete.
func (*Instance) Close ¶
Close gracefully tears down the instance, interrupting any currently executing logic and freeing the underlying WebAssembly memory.
Subsequent operations on this Instance will return ErrClosed.
func (*Instance) DefineFunction ¶
DefineFunction binds a Go function to a global Python name, letting the guest call back into the host.
Arguments arrive already converted to native Go values, following the same rules as the Type Conversion table in reverse; a []any holds a Python list or tuple, and a map[any]any holds a dict. The returned value is converted back using those same rules, so a Value built with Of, Tuple, Str and friends controls the Python type precisely.
Returning an error raises an exception at the Python call site: HostError carrying the error's text, or a specific builtin class if the error came from Raise. HostError subclasses RuntimeError, so guest code can catch host-side failures specifically with "except HostError" while a general "except RuntimeError" still sees them. A panic inside fn is recovered and raised the same way rather than unwinding into the interpreter.
The binding is part of interpreter state, so it persists for the life of the Instance and is inherited by any Clone taken afterwards. Redefining a name replaces it.
Example ¶
DefineFunction lets Python call back into Go. The binding is part of the interpreter's state, so it stays available to everything that runs after it.
package main
import (
"context"
"fmt"
"log"
micropython "github.com/gregfurman/micropython-go"
)
func main() {
ctx := context.Background()
in, err := micropython.NewInstance(ctx)
if err != nil {
log.Fatal(err)
}
defer in.Close()
rates := map[string]float64{"EUR": 1.09, "GBP": 1.27}
// Arguments arrive as native Go values; the result is converted back.
err = in.DefineFunction(ctx, "usd", func(args []any) (any, error) {
code, ok := args[0].(string)
if !ok {
return nil, fmt.Errorf("usd: want a currency code, got %T", args[0])
}
rate, ok := rates[code]
if !ok {
return nil, micropython.Raise("KeyError", code)
}
return rate * float64(args[1].(int64)), nil
})
if err != nil {
log.Fatal(err)
}
out, err := in.Exec(ctx, `
for code in ("EUR", "GBP", "JPY"):
try:
print(code, round(usd(code, 100), 2))
except KeyError as e:
print(code, "no rate for", e)
`)
if err != nil {
log.Fatal(err)
}
fmt.Print(out)
}
Output: EUR 109.0 GBP 127.0 JPY no rate for JPY
Example (Error) ¶
A host function that fails for a reason with no Python equivalent raises HostError, so guest code can catch host-boundary failures on their own without also catching the interpreter's errors.
package main
import (
"context"
"errors"
"fmt"
"log"
micropython "github.com/gregfurman/micropython-go"
)
func main() {
ctx := context.Background()
in, err := micropython.NewInstance(ctx)
if err != nil {
log.Fatal(err)
}
defer in.Close()
if err := in.DefineFunction(ctx, "fetch", func([]any) (any, error) {
return nil, errors.New("connection refused")
}); err != nil {
log.Fatal(err)
}
// In Python, as HostError.
out, err := in.Exec(ctx, `
try:
fetch()
except HostError as e:
print("guest caught:", e)
`)
if err != nil {
log.Fatal(err)
}
fmt.Print(out)
// In Go, as an ordinary error carrying the same text.
var exc *micropython.PythonError
if _, err := in.Eval(ctx, "fetch()"); errors.As(err, &exc) {
fmt.Printf("host saw: %s / %s\n", exc.Type(), exc.Message())
}
}
Output: guest caught: connection refused host saw: HostError / connection refused
func (*Instance) Err ¶
Err reports whether the interpreter is in an unrecoverable state.
It returns nil if the instance is healthy. If the WebAssembly VM experienced a fatal trap (like memory corruption) or the instance was explicitly Closed, this returns the corresponding error.
func (*Instance) Eval ¶
Eval evaluates a single Python expression (e.g., "1 + 1" or "my_dict['key']") and returns the resulting native Go value.
Unlike Exec, Eval cannot execute multi-line statements or variable assignments.
func (*Instance) Exec ¶
Exec runs arbitrary Python source code as a script and returns whatever the script printed to stdout.
Variables, imports, and functions defined during Exec will remain available in the Instance for future calls to Eval, Exec, or Call.
Example ¶
Exec returns whatever the source printed, for code written to print rather than return.
package main
import (
"context"
"fmt"
"log"
micropython "github.com/gregfurman/micropython-go"
)
func main() {
ctx := context.Background()
in, err := micropython.NewInstance(ctx)
if err != nil {
log.Fatal(err)
}
defer in.Close()
out, err := in.Exec(ctx, "for i in range(3):\n print('line', i)\n")
if err != nil {
log.Fatal(err)
}
fmt.Print(out)
}
Output: line 0 line 1 line 2
type Option ¶
type Option interface {
ProgramOption
// contains filtered or unexported methods
}
Option configures an Instance or a Program.
func WithGlobals ¶
WithGlobals binds each name before the source runs, which is how configuration reaches it without being spliced into the text.
Example ¶
Globals reach the source without being spliced into its text, so nothing has to be quoted or escaped.
package main
import (
"context"
"fmt"
"log"
micropython "github.com/gregfurman/micropython-go"
)
func main() {
ctx := context.Background()
p, err := micropython.CompileSource(ctx, `
def describe():
return "%s allows %d retries" % (NAME, LIMITS["retries"])
`, micropython.WithGlobals(micropython.Globals{
"NAME": micropython.Str("service"),
"LIMITS": micropython.Dict(micropython.Item{Key: micropython.Str("retries"), Val: micropython.Int(3)}),
}))
if err != nil {
log.Fatal(err)
}
defer p.Close()
got, err := p.Call(ctx, "describe")
if err != nil {
log.Fatal(err)
}
fmt.Println(got)
}
Output: service allows 3 retries
func WithHeapSize ¶
WithHeapSize sets the interpreter's Python heap, in bytes. Zero takes the module's default.
The heap is most of what an interpreter costs: creating one and rewinding it between calls are both proportional to it, so a program that does not need much is markedly cheaper with less. Too little and the guest raises MemoryError.
func WithHostFunc ¶
WithHostFunc binds a Go function to a global Python name before the source runs, so module-level code can call it. Equivalent to DefineFunction, except that a Program registers it ahead of its snapshot and so keeps it across calls; a Program has no other way to define one.
Repeated use accumulates. Binding the same name twice keeps the last.
A Program registers fn once, before the snapshot, so every pooled interpreter shares that one closure. What fn closes over is Go state: the per-call rewind does not reset it, and pooled interpreters call it in parallel.
var n atomic.Int64 // atomic: called from several interpreters at once
p, _ := micropython.CompileSource(ctx, "def f(): return tick()",
micropython.WithHostFunc("tick", func([]any) (any, error) {
return n.Add(1), nil
}))
p.Call(ctx, "f") // 1
p.Call(ctx, "f") // 2, not rewound to 1
func WithSourceScript ¶
WithSourceScript runs src at module level once, before anything else calls in. Globals and host functions are bound first, so src can use them. A Program snapshots the result, so every call starts from it.
micropython.CompileSource(ctx, src) // shorthand for micropython.Compile(ctx, micropython.WithSourceScript(src))
type Program ¶
type Program struct {
// contains filtered or unexported fields
}
Program is a pool of pre-compiled MicroPython interpreters, safe for concurrent use.
Compile builds one interpreter, configures it with the given options, and snapshots it. That snapshot is the baseline every later call starts from: a call borrows an interpreter, runs, and the interpreter is rewound to the snapshot before returning to the pool, so calls cannot see each other's changes to Python state.
Close must be called when the Program is no longer needed.
func Compile ¶
func Compile(ctx context.Context, opts ...ProgramOption) (*Program, error)
Compile builds an interpreter from opts and captures it as the Program's starting state. Use CompileSource to run Python source as part of that state.
WithPoolSize sets how many interpreters stay idle between calls, defaulting to runtime.NumCPU; it is not a ceiling on how many exist at once.
func CompileSource ¶
CompileSource runs src at module level and captures the result as the Program's starting state. Shorthand for Compile with WithSourceScript.
func (*Program) Call ¶
Call invokes a named Python function with the provided arguments.
The function executes in total isolation. Any changes made to Python's global state during the execution are discarded before the underlying interpreter is returned to the internal pool.
Example (Cancellation) ¶
A runaway guest stops when the context does. There is no scheduler inside the module, so this is what bounds a call.
package main
import (
"context"
"errors"
"fmt"
"log"
"time"
micropython "github.com/gregfurman/micropython-go"
)
func main() {
p, err := micropython.CompileSource(context.Background(), `
def spin():
while True:
pass
def add(a, b):
return a + b
`)
if err != nil {
log.Fatal(err)
}
defer p.Close()
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
if _, err := p.Call(ctx, "spin"); errors.Is(err, context.DeadlineExceeded) {
fmt.Println("stopped at the deadline")
}
// And the Program is usable afterwards.
got, err := p.Call(context.Background(), "add", micropython.Int(20), micropython.Int(22))
if err != nil {
log.Fatal(err)
}
fmt.Println(got)
}
Output: stopped at the deadline 42
Example (Concurrent) ¶
Calls run in parallel. A Program keeps a pool of interpreters restored from the same snapshot, so callers do not queue behind each other, and no call can see what another one did.
package main
import (
"context"
"fmt"
"log"
"sort"
"sync"
micropython "github.com/gregfurman/micropython-go"
)
func main() {
ctx := context.Background()
p, err := micropython.CompileSource(ctx, `
_seen = 0
def visit(n):
global _seen
_seen += 1
return [n * 2, _seen]
`)
if err != nil {
log.Fatal(err)
}
defer p.Close()
var (
mu sync.Mutex
doubled []int
wg sync.WaitGroup
)
for i := range 8 {
wg.Go(func() {
got, err := p.Call(ctx, "visit", micropython.Int(int64(i)))
if err != nil {
return
}
out := got.([]any)
mu.Lock()
defer mu.Unlock()
// out[1] is always 1: each call starts from the snapshot, so the
// increment another call made is not there.
doubled = append(doubled, int(out[0].(int64))+int(out[1].(int64))-1)
})
}
wg.Wait()
sort.Ints(doubled)
fmt.Println(doubled)
}
Output: [0 2 4 6 8 10 12 14]
Example (Error) ¶
A guest that raises comes back as an ordinary Go error carrying the exception's type and message.
package main
import (
"context"
"errors"
"fmt"
"log"
micropython "github.com/gregfurman/micropython-go"
)
func main() {
ctx := context.Background()
p, err := micropython.CompileSource(ctx, `
def lookup(key):
return {"a": 1}[key]
`)
if err != nil {
log.Fatal(err)
}
defer p.Close()
// The error carries the exception, so a caller can branch on which one it
// was rather than reading the message.
var exc *micropython.PythonError
if _, err := p.Call(ctx, "lookup", micropython.Str("missing")); errors.As(err, &exc) {
fmt.Println(exc.Type(), "/", exc.Message())
}
// The Program is unharmed: the failure was the guest's, not the
// interpreter's.
got, err := p.Call(ctx, "lookup", micropython.Str("a"))
if err != nil {
log.Fatal(err)
}
fmt.Println(got)
}
Output: KeyError / missing 1
func (*Program) Close ¶
Close releases every interpreter the Program is holding. Calls after it return ErrClosed.
func (*Program) Instance ¶
Instance spawns a standalone Python interpreter initialized with the compiled source's state.
Unlike a Program, an Instance is stateful: variable mutations and definitions will persist across evaluations. The returned Instance is completely detached from the Program's pool and must be closed by the caller.
type ProgramOption ¶
type ProgramOption interface {
// contains filtered or unexported methods
}
ProgramOption configures a Program. Every Option is also a ProgramOption.
func WithPoolSize ¶
func WithPoolSize(n int) ProgramOption
WithPoolSize sets how many idle interpreters a Program keeps between calls. Zero takes runtime.NumCPU.
It bounds what is kept, not what exists. A call that finds none idle builds one, and the surplus is closed on release rather than refused, so peak memory follows concurrency rather than n:
WithPoolSize(4) // a burst of 256 concurrent calls still builds 256
Each interpreter holds its own linear memory, roughly 1.2 MiB plus WithHeapSize. Set n to the concurrency you expect.
type PythonError ¶
PythonError is the error a failing call returns. Unwrap it to read which exception the guest raised, rather than matching on the message:
var exc *micropython.PythonError
if errors.As(err, &exc) && exc.Type() == "KeyError" { ... }
Exception, by contrast, builds one to send.
type Value ¶
type Value struct {
// contains filtered or unexported fields
}
Value is a Python value the host has built. It wraps the internal one so this package's surface names nothing a caller cannot import.
Example ¶
Arguments are Python values, built rather than guessed at. A Go slice could be a list, a tuple or a set, so the call says which.
package main
import (
"context"
"fmt"
"log"
micropython "github.com/gregfurman/micropython-go"
)
func main() {
ctx := context.Background()
p, err := micropython.CompileSource(ctx, "def kind(v):\n return type(v).__name__\n")
if err != nil {
log.Fatal(err)
}
defer p.Close()
for _, v := range []micropython.Value{
micropython.Int(1),
micropython.Str("hello"),
micropython.Bytes([]byte{1, 2}),
micropython.None(),
micropython.List(micropython.Int(1), micropython.Int(2)),
micropython.Tuple(micropython.Int(1), micropython.Int(2)),
micropython.Set(micropython.Int(1)),
micropython.Dict(micropython.Item{Key: micropython.Str("a"), Val: micropython.Int(1)}),
} {
got, err := p.Call(ctx, "kind", v)
if err != nil {
log.Fatal(err)
}
fmt.Println(got)
}
}
Output: int str bytes NoneType list tuple set dict
func Exception ¶
Exception builds a Python exception as a Value, for binding one the guest can raise.
Example ¶
A host can hand the guest an exception to raise, named so that the guest catches it as itself.
package main
import (
"context"
"fmt"
"log"
micropython "github.com/gregfurman/micropython-go"
)
func main() {
ctx := context.Background()
p, err := micropython.CompileSource(ctx, `
def run():
try:
raise BAD
except ValueError as e:
return "caught " + str(e)
`, micropython.WithGlobals(micropython.Globals{
"BAD": micropython.Exception("ValueError", "bad input"),
}))
if err != nil {
log.Fatal(err)
}
defer p.Close()
got, err := p.Call(ctx, "run")
if err != nil {
log.Fatal(err)
}
fmt.Println(got)
}
Output: caught bad input
func Of ¶
Of converts an ordinary Go value to its closest resembling Python representation:
Go value Python representation ------------------------------------------------------------- nil None bool bool int, int64, other integers int float32, float64 float string str []byte bytes []any list Tuple tuple Set, FrozenSet set, frozenset map[string]any, map[any]any dict anything else (e.g. structs) JSON round-trip (dict/list)
Example ¶
Of converts Go data you already have. It is the open end of the boundary: convenient, and a guess where Go has one type for two Python ones -- a slice becomes a list, never a tuple.
package main
import (
"context"
"fmt"
"log"
micropython "github.com/gregfurman/micropython-go"
)
func main() {
ctx := context.Background()
p, err := micropython.CompileSource(ctx, "def kind(v):\n return type(v).__name__\n")
if err != nil {
log.Fatal(err)
}
defer p.Close()
for _, v := range []any{
[]any{1, 2},
map[string]any{"a": 1},
struct {
A int `json:"a"`
}{1},
[]string{"x"},
} {
got, err := p.Call(ctx, "kind", micropython.Of(v))
if err != nil {
log.Fatal(err)
}
fmt.Println(got)
}
// A value with no Python equivalent reports when the call uses it, so Of
// can be written inline.
if _, err := p.Call(ctx, "kind", micropython.Of(func() {})); err != nil {
fmt.Println("error:", err)
}
}
Output: list dict dict list error: micropython: cannot pass func() to Python: json: unsupported type: func()