Skip to content

Direct Go Interop

Ard’s active backend is Go, so Ard code can import Go packages directly for APIs that map cleanly to Ard values. Use direct Go interop for thin bindings and keep companion FFI wrappers when you need domain-specific adaptation.

Use use go: to import a Go package. The package is available as a namespace, not as an Ard module.

use go:image as image
use go:net/http as gohttp

A project that imports Go packages relies on the project’s go.mod. Add Go dependencies with ordinary Go tooling, such as go get, before building the Ard project.

When a Go API needs adaptation before it is pleasant or safe to use from Ard, put a small Go package under your project’s ffi/ directory and import that package with use go:. The import path uses your Go module path, or the Ard project name when the compiler generates a minimal Go module.

my_app/
├── ard.toml
├── go.mod
├── main.ard
└── ffi/
└── env.go
ffi/env.go
package ffi
import (
"fmt"
"os"
)
func RequiredEnv(name string) (string, error) {
value, ok := os.LookupEnv(name)
if !ok {
return "", fmt.Errorf("missing environment variable: %s", name)
}
return value, nil
}
use go:my_app/ffi
fn home_dir() Str!Error {
ffi::RequiredEnv("HOME")
}

Project-local Go packages exposed to Ard should live under ffi/. Keep these packages small and use them to translate Go-specific shapes into Ard-facing APIs when direct imports are not enough.

Exported Go named types can appear directly in Ard signatures and fields.

use go:net/http as gohttp
struct RequestBox {
raw: mut gohttp::Request,
}
fn status(resp: mut gohttp::Response) Int {
resp.StatusCode
}

Raw Go pointer syntax does not appear in Ard source. Use Ard’s mutable-reference syntax instead:

  • gohttp::Response lowers to http.Response
  • mut gohttp::Response lowers to *http.Response

Go slices map to Ard lists ([T]), Go maps map to Ard maps ([K:V]), and Go fixed-size arrays map to Ard fixed-size arrays ([T; N]). The length is part of a fixed array’s type, just like in Go.

use go:crypto/sha256
let bytes = mut "hello".bytes()
let digest: [Byte; 32] = sha256::Sum256(bytes)
let zero: Byte = 0
let first = digest.at(0).or(zero)

Ard does not implicitly convert through containers. If a Go API needs [Byte] and you have [Int], write the transformation explicitly with Byte::from(...) so allocation and truncation are visible in source.

Representable single-level Go pointer parameters (*T where T is not an interface), plus Go slice and map parameters, require actual Ard references. Binding an ordinary value with mut does not satisfy that requirement; use mut expression or pass an existing reference.

use go:sort
let numbers = [3, 1, 2]
sort::Ints(mut numbers)
let number_reference = mut numbers
sort::Ints(number_reference)

The source-level reference requirement is separate from Go’s raw ABI:

  • a representable single-level Go *T receives the current *T pointer;
  • exact Go *Interface parameters remain unsupported;
  • multi-level pointers can flow only from an already compatible foreign pointer value—pure Ard cannot create them by applying another mut;
  • a Go []T or map[K]V receives the current descriptor value from an Ard reference;
  • a Go *[]T or *map[K]V receives the pointer to an Ard list or map descriptor;
  • Go functions and channels remain ordinary values.

Each boundary copies the selected current pointer or descriptor. Later rebinding of an Ard reference slot does not retarget a value already passed to or retained by Go. Foreign code receiving a pointer may replace its pointee; this is part of the explicit FFI trust boundary.

A mut Slice<T> reference projects to a compatible Go []T parameter, with capacity restricted to the view’s visible length. It does not satisfy Go *[]T: replacing that descriptor would violate the fixed-length Slice<T> contract. Convert the view with to_list() when a pointer-to-slice API must be used.

T::from(value) converts a numeric value into a bare sized scalar (Int64, Uint32, Float32, …) or a foreign named scalar type (a Go named type whose underlying type is numeric, like time::Duration). It is a truncating conversion, mirroring Go’s T(x), and returns T — not an optional — so it composes with arithmetic:

use go:time
fn every(ms: Int) time::Duration {
time::Duration::from(ms) * time::Millisecond
}
let page: Uint32 = Uint32::from(count)

Runtime values are truncated at the boundary exactly like Go. A numeric literal, however, is range-checked against the target, so a constant that cannot fit is a compile error (again matching Go’s constant conversion):

Uint8::from(200) // ok
Uint8::from(300) // error: Integer literal 300 overflows Uint8

Numeric literals already adopt a foreign scalar type directly in arithmetic and annotated bindings (let d: time::Duration = 5 * time::Millisecond); from is for converting runtime values.

Direct Go variadic functions and methods can be called with repeated trailing arguments. Ard expands those arguments at the Go call site.

use go:fmt
fmt::Println()
fmt::Println("hello")
fmt::Println("hello", 42, true)

Forward an existing list by spreading it as the final argument:

use go:os/exec as exec
let args = ["-l", "/tmp"]
exec::Command("ls", args...)
let args_reference = mut args
exec::Command("ls", args_reference...)

Spread forwards the current Go slice descriptor without copying. List<T>, Slice<T>, compatible named Go slices, and references to those values are supported when the complete slice is assignable to the variadic tail without element conversion. For example, [Any] can spread into ...Any, but [Str] cannot.

The Go function may retain the slice or mutate its elements; those element writes remain visible through the Ard list even when the list itself was passed without mut. Spread does not grant access to rebind the Ard variable or replace its descriptor.

A spread call supplies every fixed parameter positionally, followed by one final spread. It cannot use named arguments or mix individual variadic values with a spread.

Variadic elements that themselves require slice/map descriptor adaptation, such as Go ...[]T, cannot be spread yet because that ABI distinction does not survive every first-class callable path. Pass those elements individually. Ard-native variadic function declarations remain unsupported.

Named Go interfaces can be used as direct Go types. Ard checks assignability with Go’s interface rules for direct-Go values, similar to Ard trait compatibility, while generated code keeps native Go interface values.

use go:io
use go:strings
fn read_all(reader: io::Reader) [Byte]!Error {
io::ReadAll(reader)
}
let bytes = read_all(strings::NewReader("hello")).expect("read")

Interface-to-interface assignability also follows Go’s rules, so a value such as io::ReadCloser can be used where io::Reader or io::Closer is expected when the required methods match. Go slices and maps remain invariant: [mut strings::Reader] is not automatically converted to []io.Reader.

At an interface destination—including Ard Any and named empty Go interfaces—an ordinary Ard value contributes a value copy, while an existing mut T contributes its current pointer identity. Use reference.@ to deliberately select the ordinary shallow-value path instead. Every conversion copies the selected current pointer or value, so later rebinding of the Ard reference slot is not visible through an interface value already created.

A concrete mut T appears to Go as dynamic *T. Trait and mut Trait use the same native Go interface; mutable trait values widened from concrete references therefore carry dynamic *T directly. Copying a trait captures its current interface value, and later rebinding of the source trait variable is not observed. Mutable trait values pass through compatible Go generic arguments, results, containers, and method constraints without a wrapper representation.

A reference to foreign-interface storage is different: it contributes a pointer-to-interface to Any and requires interface_reference.@ when the destination needs the interface value itself.

A bare imported Go generic such as func Identity[T any](T) T infers a reference argument as its pointer-shaped representation. If T is explicitly fixed to an ordinary value type, use .@:

let echoed_reference = ffi::Identity(reference)
let copied_value = ffi::Identity<User>(reference.@)

Exclusively slice/map-shaped generic parameters still require a reference but project the exact descriptor value required by the instantiated Go signature.

Ard-defined structs can satisfy nonempty Go interfaces when their impl methods have Go-compatible method names and signatures. The Go backend emits receiver methods for those impls, including methods that are only needed by Go interface dispatch. Functions and closure adapters still need companion FFI wrappers.

Go’s predeclared error interface maps to builtin Error in parameters, fields, and other value positions:

struct AppError {
message: Str,
}
impl Error for AppError {
fn error() Str {
self.message
}
}
let error: Error = AppError{message: "request failed"}

Error::new("message") creates a simple Go-compatible error. An explicit impl Error emits Go’s Error() string method, so the Ard struct can be passed directly to Go APIs accepting error.

Conventional Go error returns preserve the original interface value: Go error becomes Void!Error, and (T, error) becomes T!Error. Error-returning Go callbacks use the corresponding !Error Ard function types. Ard functions returning Void!Error or T!Error use Go’s idiomatic error return ABI and forward the underlying error value directly.

This preservation keeps sentinel identity, concrete error types, and unwrap chains available to APIs such as Go’s errors.Is:

use go:errors
use go:io/fs
use go:os
match os::Open("missing") {
err(error) => errors::Is(error, fs::ErrNotExist),
ok(_) => false,
}

Go’s sole-error signature is inherently ambiguous. Ard consistently treats it as Void!Error, so an idiosyncratic value-returning function such as errors.Unwrap(error) error yields a non-nil returned error through the err(error) arm and nil through ok(()). A pure Ard wrapper can expose that operation as Error? when desired.

Pure Ard functions returning ordinary Error still return an ordinary value; only an explicit T!Error has Result semantics. Use error.error() or Result.map_err when a message value is specifically required. String interpolation observes Error automatically without making it assignable to Str.

Exported Go struct fields use ordinary dot syntax. Field names match Go exactly, including casing.

use go:image as image
fn min_x(rect: image::Rectangle) Int {
rect.Min.X
}

Pointer-typed hops preserve Go behavior. If an intermediate pointer is nil, the generated Go selector may panic.

use go:net/http as gohttp
fn request_path(req: mut gohttp::Request) Str {
req.URL.Path
}

Assignments to exported Go fields also use ordinary field syntax. The target must be an actual reference or foreign Go pointer. A writable ordinary mut T binding can be replaced as a whole, but it does not permit field writes or pointer-receiver calls.

use go:net/http as gohttp
fn mark_ok(resp: mut gohttp::Response) {
resp.StatusCode = gohttp::StatusOK
}

Scalar values use the same direct-Go conversion and range-check rules used at Go call boundaries.

Construct direct Go structs with keyed literals.

use go:image as image
let point = image::Point{X: 10, Y: 20}
let partial = image::Point{X: 7} // Y receives its Go zero value, 0
let rect = image::Rectangle{
Min: image::Point{X: 0, Y: 0},
Max: image::Point{X: 80, Y: 24},
}

Generic Go structs support explicit type arguments and inference from supplied fields. Given a Go type such as:

type Box[T any] struct {
Value T
}

Ard can construct it either way:

let explicit = ffi::Box<Str>{Value: "hello"}
let inferred = ffi::Box{Value: "hello"}

Use explicit type arguments when omitted fields leave a type parameter uninferred. Supplied type arguments and fields must satisfy the Go type’s constraints.

Rules:

  • any subset of exported, Ard-visible fields may be provided;
  • omitted fields receive their Go zero values;
  • field names must match exported Go field names exactly;
  • unexported fields cannot be set;
  • exported embedded fields use their declared field name; promoted child-field shorthand is not supported;
  • unsupported field types reject the literal.

Prefer Go constructors or companion wrappers for structs whose zero value is unsafe or whose invariants live in unexported fields.

Nullable mutable references are written with grouping:

use go:net/http as gohttp
let missing: (mut gohttp::Request)? = Maybe::new()

Use (mut T)? when an Ard API intentionally models an optional reference. Direct-Go pointer fields and pointer-returning calls are not automatically wrapped in Maybe; Go pointer values remain mut go::T and preserve Go nil behavior.

Use pointer.@ when an ordinary Go value is required. This makes a shallow value copy and panics with Go’s normal behavior when the foreign pointer is nil. In contrast, unsafe::cast<T>(boxed_pointer) is a fallible checked conversion and returns none for nil.

Use ard/unsafe::is_nil when you need to test a Go value for nil without adding a new Ard nil literal.

use ard/unsafe
use go:net/http as gohttp
fn request_path(req: mut gohttp::Request) Str {
match unsafe::is_nil(req.URL) {
true => "",
false => req.URL.Path,
}
}

unsafe::is_nil is a compiler-backed stdlib intrinsic. It returns false for values whose Go representation cannot be nil.

The argument expression is evaluated before is_nil runs. For example, unsafe::is_nil(req.URL) can still panic first if req itself is nil.

Use unsafe { ... } as an explicit escape hatch around direct Go operations that may panic. If the block’s final value has type T, the unsafe block has type T!Str.

use go:net/http as gohttp
fn request_path_or_default(req: mut gohttp::Request) Str {
try unsafe {
req.URL.Path
} -> _ {
""
}
}

unsafe recovers panics in the same goroutine and converts them to Str errors. It does not undo partial mutation, and break is currently rejected inside unsafe blocks.

Direct Go interop is intentionally incremental. Current limitations include:

  • embedded/promoted Go fields are not resolved through promotion;
  • Ard functions and closures cannot implement Go callback-shaped interfaces directly yet;
  • exact Go *Interface parameters are unsupported;
  • pure Ard cannot construct multi-level Go pointers—an exact compatible foreign pointer must already exist.