extern "native")vv provides an integration API to call host language functions (such as Go) from within the
vv environment using the extern declaration.
extern "native" fun system_clock()
extern "native" let system_name
When declared in this manner, the interpreter looks for a host-registered implementation by that name rather than
executing vv source code.
On the Go host side, native functions and values must be registered with the interpreter state before
vv code is evaluated. There are two patterns:
WithNative)Call builder.WithNative(name, value) (or builder.WithNatives(map) for a batch)
during builder initialization with an *interp.StateBuilder. Any interp.Value can be registered — most commonly
a interp.VBuiltinFun wrapping a Go function.
Go side:
s := interp.NewStateBuilder(cfg, sourcePath).
WithNative("system_clock", interp.VBuiltinFun(SystemClock)).
WithNative("system_name", interp.VList{...}). // any Value
Build()
// Or register several at once:
s := interp.NewStateBuilder(cfg, sourcePath).
WithNatives(map[string]interp.Value{
"system_clock": interp.VBuiltinFun(SystemClock),
"system_name": interp.StringToValue("linux"),
}).
Build()
vv side:
extern "native" fun system_clock()
extern "native" let system_name
let t = system_clock()
let os = system_name
WithModule)For standard-library style modules, register a whole map of natives keyed by the module's logical path. The interpreter will automatically load them when that module is imported or tested.
Go side:
s := interp.NewStateBuilder(cfg, sourcePath).
WithModule("std/console.vv", map[string]interp.Value{
"print": interp.VBuiltinFun(Print),
}).
Build()
// Or register several modules at once:
s := interp.NewStateBuilder(cfg, sourcePath).
WithBuiltinModules(map[string]map[string]interp.Value{
"std/console.vv": {"print": interp.VBuiltinFun(Print)},
"std/math.vv": {"pi": interp.VFloat(3.14159)},
}).
Build()
vv side (std/console.vv):
pub extern "native" fun print(value)
VUserData)Native Go functions can wrap arbitrary Go values in a VUserData struct in order to pass opaque
handles (such as file descriptors, network connections, or any other Go object) back into vv code.
The vv runtime treats VUserData as a black box — it cannot be inspected or modified
from within vv, but it can be stored in variables and records, and passed back to other native
functions.
This is the recommended pattern for representing resources that have native lifecycle semantics (open/close, connect/disconnect, etc.).
// Wrapping a Go value
func FileOpen(s *interp.State, args []interp.Value) (interp.Value, error) {
f, err := os.Open(args[0].Str())
if err != nil {
return interp.ErrorValue(err.Error()), nil
}
return interp.OkValue(&interp.VUserData{Value: f}), nil
}
// Unwrapping a Go value
func FileClose(s *interp.State, args []interp.Value) (interp.Value, error) {
fd, ok := args[0].(*interp.VUserData)
if !ok {
return nil, fmt.Errorf("expected user data")
}
f := fd.Value.(*os.File)
return interp.NoneValue, f.Close()
}
import file from "std/fs/file.vv"
let fd = file.open_read("data.txt")!
// fd holds an opaque VUserData wrapping a *os.File
file.close(fd)