← Back to the index

10. Native Interoperability

In this chapter

Native Functions (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.

Registering Native Values

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:

1. Registering individual natives (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

2. Registering a builtin module (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)

User Data (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.).

Go side (native function)

// 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()
}

vv side

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)