The Runtime Extension System
A BSP can ship Go code that runs inside your application on the device. This is how board-specific hardware support — network management, Wi‑Fi, displays, or anything else a particular board needs — gets exposed to your frontend without baking it into either the Strux runtime or your app.
This page explains the model. For the step-by-step of writing one, see Writing Runtime Extensions.
Why extensions exist
Your application is a single Go binary built from your main.go. The Strux runtime gives it a set of built-in services (Boot, Display, Network, WiFi, and so on — see the Go runtime reference). But the implementation of something like "list Wi‑Fi networks" depends entirely on the board: one device uses NetworkManager, another a vendor daemon, another has no Wi‑Fi at all.
Extensions let the BSP supply that implementation. The BSP author writes a Go package; Strux links it into your application at build time; and its methods appear on the frontend bridge alongside the built-in services. Your frontend code calls them the same way it calls everything else.
Two mechanisms, one result
There are two ways a BSP package can plug in — providers (filling in a built-in service like Network) and generic extensions (adding a brand-new namespace). Both end up on the same frontend bridge. They're covered below.
The lifecycle of an extension
bsp.yaml Strux build Go compile On device
───────── ─────────── ────────── ─────────
runtime: generates package init() registry holds
extensions: ──────▶ strux_bsp_runtime ───▶ runs, calls ──────▶ the instance;
- path: ... _extensions.go Register…() methods exposed
(blank imports) to the frontend
1. Declaration in bsp.yaml
A BSP lists its extension packages under bsp.runtime.extensions:
runtime:
extensions:
- path: runtime/network
- path: runtime/wifi
Each entry needs either path or import (validated by the schema in bsp-yaml.ts):
| Field | Meaning |
|---|---|
path | Path to a Go package, relative to the BSP directory (e.g. runtime/network → bsp/<name>/runtime/network/). Strux derives the Go import path from your project's go.mod module. |
import | An explicit Go import path, used instead of deriving one. Use this for a package that lives outside the project. |
bsp.runtime.compatible_strux_api is also part of this block — see API compatibility below.
2. Code generation at build time
During the build, Strux writes a generated file into your project root:
// Code generated by Strux; DO NOT EDIT.
package main
import (
_ "test/bsp/ht109-rk3576s/runtime/network"
_ "test/bsp/ht109-rk3576s/runtime/wifi"
)
This file does one thing: blank-import every extension package the active BSP declares. A blank import (_ "...") pulls the package into the build purely for its side effects — namely, its init() function. Without this generated file, the linker would drop packages nothing references, and the extensions would never register.
It's regenerated automatically whenever the application is compiled, so it always matches the BSP you're building for. Don't edit it — it's overwritten on every build, and switching BSPs rewrites its contents.
3. Registration via init()
Each extension package registers itself in a standard Go init() function, which runs once when the binary starts:
package network
import struxruntime "github.com/strux-dev/strux/pkg/runtime"
type Provider struct{}
func init() {
struxruntime.RegisterNetworkProvider(&Provider{})
}
func (p *Provider) ListInterfaces() ([]struxruntime.NetworkInterface, error) {
// board-specific implementation
}
The registration call hands an instance to the Strux runtime's registry. From that point on, the runtime knows about the extension and can route calls to it.
Providers vs. generic extensions
These are the two registration paths. They differ in what they plug into, not in how the frontend reaches them.
Providers — implement a built-in service
Strux defines provider interfaces for the hardware-backed services it ships: DisplayProvider, NetworkProvider, and WiFiProvider. The built-in Network/WiFi/Display services are thin shells; the provider supplies the real behavior for the board.
func init() {
struxruntime.RegisterNetworkProvider(&Provider{})
}
Use a provider when the board implements a capability Strux already has a service for. The methods land under the existing strux.network, strux.wifi, or strux.display namespace, so frontend code written against the standard services just works — the board fills in the blanks.
Generic extensions — add a new namespace
For board capabilities Strux has no built-in service for (GPIO, a vendor sensor, a custom peripheral), register a generic extension under a namespace and sub-namespace you choose:
func init() {
struxruntime.RegisterExtension("strux", "gpio", &GPIOMethods{})
}
Every exported method on the registered struct becomes callable. Registration fails if the same namespace/sub-namespace pair is registered twice, or if either is empty.
How extensions reach the frontend
Registered extensions ride the same IPC bridge as the built-in services — your frontend doesn't distinguish between them. The runtime exposes methods under a three-part name:
namespace . subnamespace . Method
When the frontend calls a method, the runtime first checks your application's own methods, then falls back to the extension registry, splitting the name into its three parts and invoking the matching method by reflection. So a registered network provider's ListInterfaces is reachable as strux.network.ListInterfaces — identical in shape to a built-in call.
Because the bridge uses reflection over exported methods, the same rules as the rest of the frontend bridge apply: export the methods you want exposed, keep signatures to JSON-serializable parameters and returns, and a trailing error becomes a rejected promise on the frontend side. The generated TypeScript types (strux types) pick these up too — see the frontend API reference.
API compatibility
A BSP can declare which Strux API versions its extensions are written against:
runtime:
compatible_strux_api: "0.3" # or a list: ["0.3", "0.4"]
At build time, Strux reads the Strux runtime version your project depends on (from go.mod), reduces it to a major.minor key, and checks it against compatible_strux_api. If the BSP doesn't list your project's API version, the build stops with a message telling you which versions the BSP claims and how to opt in:
BSP
<name>supports Strux API 0.3, but this project uses Strux API 1.2. If this BSP has been tested with 1.2, add "1.2" to bsp.yaml atbsp.runtime.compatible_strux_api.
This is a guard, not a hard lock: if you've verified a BSP works against a newer API, you add that version to the list yourself. If compatible_strux_api is omitted entirely, no check runs.
Where to go next
- Writing Runtime Extensions — the hands-on guide to building a provider or extension package.
- Go Runtime Reference — the built-in services and provider interfaces you implement.
- Frontend — how the bridge looks from the frontend side.