Skip to content

Writing libraries

Scaffolding a project

gos new example.com/widget --path widget
cd widget

You get:

widget/
├── project.toml
└── src/
    └── main.gos

--template picks what is scaffolded: bin (the default) writes an executable src/main.gos; lib writes a reusable src/lib.gos with a smoke test; service writes an HTTP handler bound to 0.0.0.0:8080; workspace writes a manifest with an empty [workspace.members] and no source tree; binding writes a Rust crate that publishes functions to Gossamer (see Calling Rust).

The project.toml manifest

[project]
id      = "example.com/widget"
version = "0.1.0"
# Which toolchain this project is written against, matching the release
# tag. A bare version names that toolchain and no other; `^v0.55.0`
# names it as a floor and accepts every later one. `gos new` stamps a
# floor. A toolchain the requirement does not accept refuses to build
# the project rather than failing later on a surface it does not have.
gossamer-version = "^v0.55.0"
authors = ["Leslie Tungsten <ltungsten@example.com>"]
license = "Apache-2.0"
# Whether the program may call C through `unsafe extern "C"` declarations,
# its own or a dependency's. Absent reads as `true`; `false` makes any
# such declaration GT0102 (see Foreign code below).
ffi = false

[dependencies]
# A bare version pins; `^1.2.3` accepts 1.2.3 or anything later.
"example.org/lib" = "1.2.3"

[registries]
default = "https://registry.example.org"

# Required before the first registry fetch for a package. The registry
# cannot establish this binding by advertising a key in its index.
[trusted-publishers]
"example.org/lib" = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"

# Optional: explicit binary targets. Without this section, the
# default is one binary named after the project id whose entry
# point is `src/main.gos`.
[[bin]]
name = "widget"
path = "src/main.gos"

# Optional: a library target alongside / in place of a binary.
# Without this section, presence of `src/lib.gos` is enough to
# build the library by convention.
# Without a `kind`, `gos build` compiles the library natively and
# links nothing: dependents build it from source.
# `name` is also the module dependents import the library as
# (`use widget`), unless their manifest names another with `module`.
[lib]
name = "widget"
path = "src/lib.gos"
# Optional: also build a C library from the `#[export]` functions - a
# static archive, a shared library, and `include/widget.h`.
kind = ["staticlib", "cdylib"]
# Optional: "host" builds a static archive without the runtime, for a host
# that already links the Gossamer runtime. Its calls resolve against the
# host's, so the process keeps one allocator and one scheduler. The default,
# "bundled", carries the runtime so a C program links the archive alone.
runtime = "bundled"

# Optional: C sources this package compiles and links (see Foreign code).
[native]
sources = ["csrc/shim.c"]
include = ["csrc/include"]

# Optional: build-time features, read by `#[cfg(feature = "..")]`.
[features]
default = ["json"]
json = []

Features

A feature turns on code marked #[cfg(feature = "name")], in an item or a use. An entry under [features] lists what it turns on in turn: another feature of the project, dep:<dependency> for an optional dependency, or <dependency>/<feature> for a dependency's own feature.

[features]
default = ["json"]
json = []
compress = ["dep:zstd_gos", "codec/fast"]

[dependencies]
zstd_gos = { git = "https://github.com/example/zstd-gos", optional = true }
codec = { path = "../codec", features = ["simd"], default-features = false }

default is on unless --no-default-features is given, and --features compress,json turns on more, on every command (gos run, gos build, gos test, gos check). Features add up across the graph: a dependency builds with every feature any dependent asks for, and each package's #[cfg(feature = ..)] reads its own features. An optional dependency joins the build only while a feature names it, so its use carries the same #[cfg]:

#[cfg(feature = "compress")]
use zstd_gos

A dependency is keyed by the project id it publishes under, or - when its source names its own identity, as a git, path, or tarball entry does - by the module name source imports it as:

[dependencies]
pgsql_gos = { git = "https://github.com/gossamer-lang/pgsql-gos" }

A library names the module it is imported as with [lib] name; without one, its module name is the final path segment of its id. A package name may carry -, which no identifier may, so each - in either is replaced by _. Every import spelling that names that module reaches the package: use pgsql_gos, use pgsql_gos::greet, use pgsql_gos::{greet}, use pgsql_gos as pg, and use "github.com/gossamer-lang/pgsql-gos". A use pgsql-gos is rejected (GP0040) - - is subtraction, never part of an identifier - and two dependencies reaching source under one module name are rejected (GR0019), which an explicit alias or a distinct key resolves.

A git source is versioned by the reference it is checked out at: tag, branch, or rev, defaulting to main. The resolved reference is written to project.lock, so a build repeats the same checkout.

[dependencies]
pgsql_gos = { git = "https://github.com/gossamer-lang/pgsql-gos", tag = "v1.2.3" }

A version requirement belongs to a registry dependency, which resolves within it; writing one beside git is rejected rather than silently ignored. There are two spellings and no third: "1.2.3" (or "=1.2.3") is exactly that version, and "^1.2.3" is that version or any later one. A bare literal pins because a manifest that names a version and gets a different one is a surprise nobody asked for, and ^ has no ceiling because a ceiling would be a guess about code that has not been written yet - project.lock is where a reproducible graph is recorded. A third source form names an archive directly, pinned by digest:

[dependencies]
"example.org/lib" = { tarball = "https://example.org/lib-1.2.3.tar.gz", sha256 = "..." }

Whichever source a dependency names, gos fetch prepares it (gos vendor copies the same trees into ./vendor/), and from then on it joins the compilation unit exactly as a path dependency does, under the module its id names. gos update refreshes selected versions within declared ranges.

gos add example.org/lib@1.2.3 appends the dependency and gos remove example.org/lib drops it. gos tidy parses project sources, removes direct project dependencies that are not imported, and writes canonical ordering; [rust-bindings] entries are reached through Rust rather than through a use, so they are retained independently.

The default convention is still: src/main.gos ⇒ binary, src/lib.gos ⇒ library, project id ⇒ output name. The [[bin]] / [lib] sections let you override the entry-point path, rename the output, or ship multiple binaries from one project.

Selecting the entry file

For a single-binary project, the optional [project] entry key names the entry source directly, overriding convention-based resolution:

[project]
id      = "example.com/widget"
version = "0.1.0"
entry   = "src/app.gos"

The path is relative to the manifest directory. The resolved entry is the only file allowed to carry top-level statements; sibling and library modules contain items only.

Module layout

A package spans files and directories:

src/
├── main.gos       # binary entry  (default; override via [[bin]].path)
├── lib.gos        # library root  (default; override via [lib].path)
├── widget.gos     # submodule `widget`
└── sub/
    ├── mod.gos    # submodule `sub`
    └── deep/
        └── mod.gos  # submodule `sub::deep`

A sibling src/<name>.gos is the module name. A subdirectory is a module when it carries a mod.gos root (src/<dir>/mod.gos is the module dir), and it may nest its own sibling files and subdirectories, recursively, to any depth. Each .gos file is its own module; declare pub on anything you want visible to other modules or to dependent packages.

The layout declares the modules, so the entry needs no mod NAME; line. A module's items are not in scope on their own: name them through a path or bring them in with use.

// src/main.gos
use widget::greet

fn main() {
    println("{}", greet("world"))
    println("{}", sub::ping())
}

Writing a bare greet(..) without the import reports GR0011, which names the declaring module and the exact use line to add. A type belongs to the module that declares it, so two modules may each declare a Config without the two colliding.

// src/widget.gos
pub fn greet(name: String) -> String {
    // Reach another module from the package root with `crate::`,
    // or one level up with `super::`.
    crate::sub::banner() + ", " + name
}

A module reaches another by a navigation path: crate::other::item (rooted at the package), super::other::item (one level up), or self::child::item (a child of the current module). gos, gos build, and gos check all assemble the package the same way, so a directory argument (gos my_project) or gos check src/ checks the whole package as one unit.

Unit + integration tests

// inside src/widget.gos
pub fn add(a: i64, b: i64) -> i64 { a + b }

#[cfg(test)]
mod tests {
    #[test]
    fn add_adds() {
        let total = super::add(2, 3)
        assert(total == 5)
    }
}

Integration tests live under tests/. gos test src/lib.gos runs them on the register-based bytecode VM.

Documentation

// Pixel width of `text` at this font's current size,
// including kerning.
pub fn measure_text(&self, text: String) -> u32 { ... }

Gossamer uses one comment form: // for line comments and /* ... */ for block comments. There is no separate /// / //! doc-comment syntax - a run of // lines directly above an item (no blank line between) is its documentation, and a run at the top of a file is the module's. gos doc src/lib.gos prints every item plus that summary block; gos doc --html <path> src/lib.gos writes an HTML page instead.

Foreign code ([rust-bindings])

Native code is reached through a binding crate: an ordinary Rust library that depends on gossamer-binding, marks the functions it publishes, and is named under [rust-bindings] in project.toml.

# project.toml
[rust-bindings]
native = { path = "native" }
// native/src/lib.rs
use gossamer_binding::gos_module;

#[gos_module("native")]
mod bindings {
    /// Shout the input.
    pub fn shout(s: String) -> String {
        s.to_uppercase()
    }
}
use native
fn main() { println("{}", native::shout("hello")) }

C functions need no crate: an unsafe extern "C" block declares them directly (Foreign functions); a project refuses them with ffi = false under [project]. C the package carries itself is listed under [native] and compiled with the target's C compiler (GOS_CC, then CC), cached under .gos-cache/native/, and linked into gos run and gos build alike; a feature key in the table compiles it only with that feature. gos bindgen --c header.h writes the declarations for a C header, and #[export] with [lib] kind goes the other way, building a C library from Gossamer functions. The switch covers dependencies too: a library that declares C functions fails to build inside a project that refuses native code (GT0102). Calling Rust has the full instructions: the type vocabulary, errors, opaque handles, blocking work, wrapping a crate that knows nothing about Gossamer, and the tier and ABI rules.

Publishing

A registry is named by [registries] default in project.toml or by GOS_REGISTRY_URL; the toolchain names none of its own, and a registry dependency with none configured is an error that says so.

gos publish packs the project, signs the archive's SHA-256 digest (Ed25519, with the key at ~/.gossamer/keys/<id>.ed25519 or in GOS_PUBLISH_KEY), and uploads it to the registry; --dry-run packs and signs without uploading. A registry refuses an unsigned upload. gos yank, gos login / gos logout, and gos owner round out the registry workflow, with dependency tarballs sha256-pinned in project.lock. A registry package must also have a publisher key pinned in project.lock or explicitly bound in [trusted-publishers] before its first fetch; keys advertised only by a registry index are not trusted. Path-based and git-based dependencies in project.toml also work end-to-end.

Running a registry

gos registry serve --root DIR --addr HOST:PORT serves a registry from a directory: index/ holds each package's version list, packages/ the archives, and owners/ who may publish each id. The first publisher of an id owns it; gos owner add shares it. A tokens.toml in the root maps each user to a bearer token:

[tokens]
alice = "a-long-random-token"

Clients store theirs with gos login --registry URL. A root with no tokens.toml accepts every request as the user local, which suits a registry on one machine and nothing else, so the default address is loopback. Plain http:// is accepted for a loopback registry; anything remote is https:// behind a TLS terminator.