Embedded engine (no cgo)¶
riftembed runs a Rift engine inside your process: a Tokio runtime on its own threads, the
matching engine it drives, and optionally an admin plane served over it.
eng, err := riftembed.Start(riftembed.Options{})
if err != nil {
t.Fatal(err)
}
defer eng.Close()
port, err := eng.CreateImposter(ctx, rift.NewImposter("users").Record().
Stub(rift.OnGet("/health").Return(rift.OKText("ok"))))
Why purego and not cgo¶
cgo would have been simpler to write, and it would have poisoned the build for every consumer downstream: a C toolchain requirement, slower builds, cross-compilation pain, and larger CI images — costs paid by people who never asked for them.
purego resolves symbols through the dynamic linker at runtime instead. You give up some ergonomics at the boundary — no automatic struct marshalling, more manual work — and in exchange a Go user adds a dependency and nothing about their build changes.
The claim, precisely
CGO_ENABLED=0 go test ./... passes, including the tests that load the shared library and
serve real HTTP. This is verified in CI on Linux, macOS and Windows.
The engine's ABI is deliberately all char* and integers, which is exactly the subset purego
handles well. That is not an accident — it is what makes a binding like this possible at all.
Thread affinity¶
The engine reports failures through rift_last_error, which is thread-local. A Go goroutine
may migrate between OS threads at any call boundary, so a downcall on one thread followed by an
error read on another would read an empty error — intermittently, under load, in a way that looks
like the engine silently succeeded.
Every call in this package therefore pins its goroutine with runtime.LockOSThread for the
duration of the (downcall, error-read) pair. You do not need to do anything about this; it is
noted because it is the kind of thing that looks like an unnecessary cost until it isn't.
Finding the native library¶
riftembed looks, in order:
- an explicit
Options.LibraryPath $RIFT_FFI_LIB- the on-disk cache (
riftembed.CachePath()) ./natives,./natives/<goos>-<goarch>, and the working directory
It will not download anything implicitly. A test run that quietly reaches out to the network is a bad default, and being explicit is what makes CI and air-gapped hosts predictable. See Native library & CI.
// Report what would be loaded, without loading it. The error lists every path tried.
path, err := riftembed.LibraryPath()
Version checking¶
Start verifies the library's C-ABI version before doing anything else. A mismatch means the
library and the SDK disagree about symbol signatures and ownership, which is not something either
side can paper over at runtime:
eng, err := riftembed.Start(riftembed.Options{})
if errors.Is(err, rift.ErrVersionMismatch) {
// the installed library is for a different ABI generation
}
Options.SkipVersionCheck exists for deliberately testing against a pre-release library. Using it
otherwise is undefined behaviour, not a warning.
The same sentinel reports a serve option the loaded engine does not accept; see Serve options.
Lifecycle¶
Engine is safe for concurrent use. Close is idempotent, and calls after it fail cleanly with
rift.ErrClosed rather than using a freed handle:
eng.Close()
_, err := eng.CreateImposter(ctx, imp) // errors.Is(err, rift.ErrClosed)
Contexts¶
Every method takes a context.Context. The embedded lane checks it before the downcall and
cannot interrupt one already in flight — an FFI call is synchronous. That is documented rather
than pretended otherwise; honouring cancellation at the boundary is the honest amount of support
to offer, and it is enough for the case that matters: a cancelled test not queueing more work
against an engine it is about to close.
Serving the admin API¶
ServeAdmin puts the engine's HTTP admin API on a port, for tools that talk to it over HTTP:
raw, err := eng.ServeAdmin(ctx, riftembed.ServeOptions{APIKey: token})
// {"adminPort":49321,"adminUrl":"http://127.0.0.1:49321","metricsPort":null}
With APIKey set, clients must send it as the raw Authorization header value, which is what
rift.Connect with RemoteOptions.APIKey does. Leave it empty to run unauthenticated. A
whitespace-only key is refused before the engine is called, and engines from 0.17.0 on refuse one
too: it would switch the auth gate on and then admit every request.
Build info and capabilities¶
info, _ := eng.BuildInfo()
info.Version // "0.19.0"
info.Features // compiled features: ["redis-backend", "javascript"]
info.SupportsServeOption("noParse") // true on 0.19.0, false on 0.17.0
ServeOptions is the supported way to feature-detect. The absence of a key means an engine
too old to accept it. A rejection only ever comes from an engine that already knows the field, so
you cannot detect a new capability by watching for an error: an older engine silently ignores it.
ServeAdmin applies this check itself. An option the loaded engine does not advertise fails with
rift.ErrVersionMismatch before the engine is called. That matters most for RequireAdminAuth,
which an engine older than 0.17.0 would drop and then serve an open admin plane. Engines older than
0.17.0 publish no list; on them only the seven original options count as supported.
Serve options¶
| Field | Engine | Effect |
|---|---|---|
Host, Port |
all | Admin API bind address. Host must be an IP literal. |
APIKey |
all | Clients send it as the raw Authorization header. |
MetricsPort |
all | Serve Prometheus metrics on this port, on the same host. |
ConfigFile |
all | Load imposters from a JSON or YAML file; POST /admin/reload re-reads it. |
NoParse |
0.19.0 | Load ConfigFile verbatim, skipping EJS preprocessing. The only way to keep a literal <%. Requires ConfigFile. |
Config |
all | Apply an imposters document at serve time. |
AllowInjection |
all | Admit inject and script imposters arriving through the admin plane or ConfigFile. |
RequireAdminAuth |
0.17.0 | Refuse an off-host admin plane with no APIKey; from 0.19.0 it also governs StartIntercept. |
UpstreamCAFile, UpstreamCAPEM |
0.19.0 | Trust an extra CA, from a file or inline, for the TLS the engine dials. One or the other. |
UpstreamTLSSkipVerify |
0.19.0 | Accept any upstream certificate. Development only. |
Imposters this process hands the engine directly (CreateImposter, ApplyConfig and
ServeOptions.Config) are never gated by AllowInjection: the embedding process can already run
code here.
Trusting a private upstream CA¶
A proxy stub dials a real upstream over TLS, and so does the intercept listener's origin leg.
The engine trusts the OS trust store. An upstream issued by a private or corporate CA fails until
you add that CA:
eng.ServeAdmin(ctx, riftembed.ServeOptions{Host: "127.0.0.1", UpstreamCAPEM: corporateCA})
port, _ := eng.CreateImposter(ctx, rift.NewImposter("record").
Stub(rift.OnAny().Return(rift.Proxy("https://internal.example").Once())))
The policy is installed by ServeAdmin, and an imposter reads it when it is created. Serve
before creating the imposters that should use it. An engine that only needs the trust policy
can serve on loopback with port 0.
UpstreamCAFile and UpstreamCAPEM append to the OS trust store, and supplying both is refused.
UpstreamTLSSkipVerify accepts any certificate. It is for development only, and the engine logs a
warning. All three need engine 0.19.0 or later.
SSL_CERT_FILE replaces, it does not append
The engine honours SSL_CERT_FILE, but that variable replaces the trust store. Pointing it
at a lone private CA silently drops every public root. Use UpstreamCAFile instead.
Unsupported platforms¶
riftembed requires a platform purego can load libraries on: macOS, Linux, FreeBSD or Windows.
Anywhere else, Start returns rift.ErrEngineUnavailable and you should use
Connect or Spawn, which need no native library.