View all results

Remote runtimes (Wire)

Your application embeds Ferret and registers an app::greet function. A separate client needs to run a script that calls it, using the application’s existing runtime setup:

greeting.fql
read-only
return app::greet(@name)

Wire lets the client compile and execute this script against the hosted runtime. With name set to "Ada", it returns the JSON string "Hello, Ada!". The custom function is registered on the host; the client sends source and parameters.

Use Wire when another process needs your application’s functions, modules, services, or runtime configuration without rebuilding that setup locally.

How the pieces fit together

Ferret Core provides the native engine and its embedding configuration. The Universal API provides shared runtime, plan, session, and debugger contracts. Wire exposes a hosted implementation remotely; its Go client returns an api.Runtime.

The native adapter and Wire client let consumers use the same contracts. Runtime construction and configuration remain the host’s responsibility. See the Universal API guide for the shared programming model.

The current implementation uses protobuf and gRPC, with a Go server and client. This guide uses those implementations.

Run a host and client

The example contains two complete programs in a separate Go module. It requires Go 1.25 or later and pins these compatible versions:

Dependency Version
Ferret Core v2.0.0-alpha.57
Universal API v1.0.0-alpha.20
Wire v1.0.0-alpha.2
gRPC v1.84.0

Download the example and its dependencies:

terminal
git clone https://github.com/MontFerret/montferret.github.io.gitcd montferret.github.io/examples/wirego mod download

The module is independent of the website build. You can also copy the two programs below into host/main.go and client/main.go in a new directory, then initialize the same dependencies:

terminal
go mod init example.com/wire-demogo get github.com/MontFerret/ferret/v2@v2.0.0-alpha.57 github.com/MontFerret/api@v1.0.0-alpha.20 github.com/MontFerret/wire@v1.0.0-alpha.2 google.golang.org/grpc@v1.84.0

Host the configured engine

Save this program as host/main.go. The host registers a typed, one-argument function, constructs the native engine, wraps it through the official adapter, and serves it through Wire.

host/main.go
read-only
package main import ( "context" "errors" "flag" "fmt" "log" "net" "os" "os/signal" "syscall" "time" "github.com/MontFerret/ferret/v2" "github.com/MontFerret/ferret/v2/pkg/runtime" "github.com/MontFerret/ferret/v2/uapi" "github.com/MontFerret/wire/server" ) func main() { address := flag.String("listen", "127.0.0.1:50051", "local development endpoint") flag.Parse() ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() if err := run(ctx, *address); err != nil { log.Print(err) os.Exit(1) } } func run(ctx context.Context, address string) (resultErr error) { native, err := ferret.New(ferret.WithFunctionsRegistrar(func(ns runtime.Namespace) { ns.Namespace("app").Function().A1().Add("greet", func(ctx context.Context, arg runtime.Value) (runtime.Value, error) { if err := ctx.Err(); err != nil { return nil, err } name, err := runtime.CastArg[runtime.String](arg, 0) if err != nil { return nil, err } return runtime.NewString("Hello, " + string(name) + "!"), nil }) })) if err != nil { return err } defer func() { resultErr = errors.Join(resultErr, native.Close()) }() // Wrap and Wire borrow this engine; the host remains its owner. hosted := uapi.Wrap(native, "v2.0.0-alpha.57") wireServer, err := server.NewServer(hosted) if err != nil { return err } // Plaintext transport is for local development only. listener, err := net.Listen("tcp", address) if err != nil { return err } defer func() { if err := listener.Close(); !errors.Is(err, net.ErrClosed) { resultErr = errors.Join(resultErr, err) } }() served := make(chan error, 1) go func() { served <- wireServer.Serve(context.Background(), listener) }() serveReturned := false defer func() { shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() resultErr = errors.Join(resultErr, wireServer.Shutdown(shutdownCtx)) if !serveReturned { select { case err := <-served: resultErr = errors.Join(resultErr, err) case <-shutdownCtx.Done(): resultErr = errors.Join(resultErr, shutdownCtx.Err()) } } }() if _, err := fmt.Printf("Listening on %s\n", listener.Addr()); err != nil { return err } select { case resultErr = <-served: serveReturned = true case <-ctx.Done(): // Shutdown settles Wire-owned work before the host closes its engine. } return resultErr }

uapi.Wrap borrows the engine. The supplied version identifies Ferret Core, independently of the host application and Wire. The host remains responsible for shutting down Wire and closing the engine.

Start the host in the example directory:

terminal
go run ./host

It prints:

Listening on 127.0.0.1:50051

This example uses local-development-only plaintext transport on loopback. Loopback binding limits network exposure; it does not authenticate clients. Configure deployment security before exposing an endpoint beyond this example.

Execute from another process

Save this program as client/main.go. It constructs its transport connection and follows the normal Universal API flow: Compile → NewSession → Run.

client/main.go
read-only
package main import ( "context" "errors" "flag" "fmt" "log" "os" "os/signal" "syscall" "time" "github.com/MontFerret/api" "github.com/MontFerret/wire/client" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) func main() { address := flag.String("address", "127.0.0.1:50051", "host endpoint") name := flag.String("name", "Ada", "greeting parameter") timeout := flag.Duration("timeout", 10*time.Second, "maximum query duration") flag.Parse() ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() ctx, cancel := context.WithTimeout(ctx, *timeout) defer cancel() if err := run(ctx, *address, *name); err != nil { log.Print(err) os.Exit(1) } } func run(ctx context.Context, address, name string) (resultErr error) { // Plaintext transport is for local development only. conn, err := grpc.NewClient(address, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { return err } defer func() { resultErr = errors.Join(resultErr, conn.Close()) }() remote, err := client.New(ctx, conn) if err != nil { return err } defer func() { resultErr = errors.Join(resultErr, remote.Close()) }() plan, err := remote.Compile(ctx, api.NewSource("greeting.fql", "return app::greet(@name)")) if err != nil { return err } defer func() { resultErr = errors.Join(resultErr, plan.Close()) }() session, err := plan.NewSession(ctx, api.WithParam("name", name), api.WithOutputContentType("application/json"), ) if err != nil { return err } defer func() { resultErr = errors.Join(resultErr, session.Close()) }() output, err := session.Run(ctx) // Available output can accompany an execution or cleanup error. if output != nil { _, writeErr := fmt.Printf("Content-Type: %s\n%s\n", output.ContentType, output.Content) err = errors.Join(err, writeErr) } return err }

In another terminal, from the same example directory, run:

terminal
go run ./client

The output is:

Content-Type: application/json
"Hello, Ada!"

The client has no native engine or function registration. app::greet runs on the host with the supplied parameter. To change it, run go run ./client -name Grace.

Both programs handle Ctrl+C and termination signals. The client bounds its operations with a ten-second context; use -timeout to change that bound. Cancellation ends work but does not replace explicit resource cleanup. Execution and cleanup errors are reported with a nonzero exit status, and available output is inspected independently of an error.

Stop the host with Ctrl+C. It uses a fresh five-second shutdown context to settle Wire-owned work before closing the native engine. For a different local port, start go run ./host -listen 127.0.0.1:50052 and use go run ./client -address 127.0.0.1:50052. The -listen, -address, -name, and -timeout flags belong to these example programs.

Connect with the CLI

A Wire-capable Ferret CLI can execute source, open a REPL, and report the host’s version against this example:

terminal
ferret run --runtime wire --runtime-endpoint tcp://127.0.0.1:50051 --eval 'RETURN app::greet(@name)' --param name=Adaferret repl --runtime wire --runtime-endpoint tcp://127.0.0.1:50051ferret version --runtime wire --runtime-endpoint tcp://127.0.0.1:50051

The CLI uses the host’s functions, modules, policies, and filesystem configuration. It supports source execution through Wire; artifacts and ferret debug require the builtin runtime. The CLI Wire guide covers its loopback-only endpoint syntax, timeout, compatibility, diagnostics, and ownership.

What crosses the remote boundary

Registered functions and modules, host services, and runtime configuration stay on the host. Wire does not construct that runtime or transfer its configuration to the client.

Portable interfaces do not make every Go value transportable. Wire parameters support null, booleans, signed integers, unsigned integers that fit in int64, finite floats, strings, bytes, []any, and map[string]any. Arbitrary Go functions, native host objects, datetime, duration, and regexp values cannot be sent as parameters. See the client’s parameter rules for conversion details and limits.

Execution returns an api.Output with a content type and encoded bytes. The host selects an available encoder, and Wire preserves its output without decoding it. A client may decode those bytes according to the content type. Returning encoded data does not transfer a live host object or its capabilities.

Wire or Worker?

Use Worker when you want to deploy the HTTP execution service with its supported runtime configuration and service controls.

Use Wire when your application already owns a configured runtime and needs to expose compilation, execution, and debugging through the Universal API. Wire is an integration library and protocol around that runtime. Your application owns the endpoint and deployment concerns.

Debug a hosted script

Wire supports the Universal API debugging path: compile with remote.CompileDebug, then create a debugger with plan.NewDebugSession. The returned debugger session supports entry stops, breakpoints, stepping, inspection, and evaluation through the hosted implementation. Close the debugger and plan when finished.

See the Wire client debugger documentation and Universal API guide for the complete API. Wire’s debugging capability does not imply that a particular editor or CLI can connect to its endpoint; that requires a tool-specific integration.

Own resources and secure the endpoint

The host owns its runtime and endpoint. Wire borrows the runtime and does not close it. The client owns its transport connection; closing the remote runtime leaves that connection open.

Close every plan, normal session, and debug session you create. Settle work first, then close sessions before plans, the remote runtime, and the transport. Ordinary parent closure is not a substitute for closing caller-owned descendants. The example preserves cleanup errors with errors.Join.

Deployment security is an integration responsibility. Authenticate callers and authorize the compilation, execution, and debugging capabilities they can use. Protect transport traffic with an appropriate authenticated deployment boundary, or use a private socket with restrictive directory and socket permissions. Restrict the host functions, modules, filesystem, and network services exposed to scripts according to your application’s needs. A loopback address alone is not authentication or isolation between local users.

Wire is pre-stable. Pin a compatible set of Core, Universal API, and Wire versions, and verify changes before upgrading. The ferret.wire.v1 protobuf package name does not guarantee a stable public release.

Next steps