Aether Standard Library Guide
Overview
Aether provides a standard library for strings, I/O, math, file system, networking, and actor concurrency. The library is automatically linked when you compile Aether programs.
Go-style result types. Every stdlib function that can fail returns a(value, err)tuple. Checkerrfirst, then usevalue. The raw C-style externs are preserved under a_rawsuffix for advanced callers who need direct access to the underlying pointer or int: ``aether body, err = http.get("http://example.com") if err != "" { println("failed: ${err}") return } println(body)`See the [error handling example](../examples/basics/error-handling.ae) for the user-function pattern, andexamples/stdlib/http-client.ae` for the stdlib pattern.
Namespace Calling Convention
Functions are called using namespace-style syntax: namespace.function()
| Import | Namespace | Example Call |
|---|---|---|
import std.string | string | string.new("hello"), string.release(s) |
import std.file | file | file.exists("path"), file.open("path", "r") |
import std.dir | dir | dir.exists("path"), dir.create("path") |
import std.path | path | path.join("a", "b"), path.dirname("/a/b") |
import std.json | json | json.parse(str), json.create_object() |
import std.cryptography | cryptography | cryptography.sha256_hex(data, n), cryptography.base64_encode(data, n) |
import std.http | http | http.get(url), http.server_create(port) |
import std.tcp | tcp | tcp.connect(host, port), tcp.write(sock, data) |
import std.list | list | list.new(), list.add(l, item) |
import std.map | map | map.new(), map.put(m, key, val) |
import std.set | set | set.new(), set.add(s, item) |
import std.pqueue | pqueue | pqueue.new(), pqueue.push(q, pri, item) |
import std.math | math | math.sqrt(x), math.sin(x) |
import std.log | log | log.init(file, level), log.write(level, msg) |
import std.io | io | io.print(str), io.read_file(path), io.getenv(name) |
import std.os | os | os.system(cmd), os.exec(cmd), os.getenv(name), os.argv0(), os_execv(prog, argv) |
Using the Standard Library
Import modules with the import statement:
import std.string // String functions
import std.file // File operations
import std.dir // Directory operations
import std.json // JSON parsing
import std.http // HTTP client & server
import std.tcp // TCP sockets
import std.list // ArrayList
import std.map // HashMap
import std.set // Unique-string set
import std.pqueue // Priority queue
import std.math // Math functions
import std.log // Logging
import std.io // Console I/O, environment variables
import std.os // Shell execution, environment variables
Call functions using namespace syntax:
import std.string
import std.file
main() {
// String operations
s = string.new("hello")
len = string.length(s)
string.release(s)
// File operations
if (file.exists("data.txt") == 1) {
size = file.size("data.txt")
}
}
Or use extern for direct C bindings:
extern my_c_function(x: int) -> ptr
String Library
Types
typedef struct AetherString {
unsigned int magic; // Always 0xAE57C0DE, enables runtime type detection
int ref_count;
size_t length;
size_t capacity;
char* data;
} AetherString;
Note: Allstd.stringfunctions accept both plainchar*strings and managedAetherString*transparently. Themagicfield is used internally to distinguish between the two at runtime.
Available Functions
Creating Strings
string_new(const char* cstr)- Create from C stringstring_from_literal(const char* cstr)- Alias for newstring_empty()- Create empty stringstring_new_with_length(const char* data, size_t len)- Create with explicit length
String Operations
string_concat(AetherString* a, AetherString* b)- Concatenate stringsstring_length(AetherString* str)- Get lengthstring_char_at(AetherString* str, int index)- Get characterstring_equals(AetherString* a, AetherString* b)- Check equalitystring_compare(AetherString* a, AetherString* b)- Compare (-1, 0, 1)
String Methods
string_starts_with()- Check prefixstring_ends_with()- Check suffixstring_contains()- Search for substringstring_index_of()- Find positionstring_substring()- Extract substringstring_to_upper()- Convert to uppercasestring_to_lower()- Convert to lowercasestring_trim()- Remove whitespace
Conversion
string.to_cstr(str)- Get C string pointerstring.from_int(value)- Convert int to stringstring.from_float(value)- Convert float to string
Parsing (Go-style)
All parsers return (value, err) tuples. Empty err means success.
n, err = string.to_int("42")
if err != "" { println("bad: ${err}"); return }
println(n)
string.to_int(s)→(int, string)- Parse base-10 integerstring.to_long(s)→(long, string)- Parse 64-bit integerstring.to_float(s)→(float, string)- Parse floatstring.to_double(s)→(float, string)- Parse double
Raw out-parameter externs are preserved as string_to_int_raw, string_to_long_raw, string_to_float_raw, string_to_double_raw for code that needs to distinguish zero from parse failure without a tuple.
Memory Management
string.new(cstr)- Allocate a new string (usestring.freewhen done)string.free(str)- Free the string
Use defer string.free(s) right after string.new() to ensure cleanup at scope exit.
The underlying C implementation also exposes string.retain() / string.release() for advanced use cases (e.g., sharing ownership across C callbacks), but Aether programs should use string.free() directly.
File System Library
Complete filesystem library with file and directory operations.
Usage
import std.file
import std.dir
main() {
// Read a file in one call (opens, reads, closes)
content, err = file.read("data.txt")
if err != "" {
println("cannot read: ${err}")
return
}
println(content)
// Write a file
werr = file.write("output.txt", "hello")
if werr != "" {
println("cannot write: ${werr}")
return
}
// Get size
size, serr = file.size("data.txt")
if serr == "" {
println("size: ${size} bytes")
}
// Create a directory
derr = dir.create("output")
if derr != "" {
println("cannot mkdir: ${derr}")
}
}
File Operations (Go-style)
file.read(path)→(string, string)- Read entire file (opens, reads, closes). Handles/proc//sysseq-files and unseekable fds via a read-to-EOF fallback (a size-0ftellno longer silently returns"").file.write(path, content)→string- Write content, return error stringfile.open(path, mode)→(ptr, string)- Low-level open (caller mustfile.close)file.close(handle)- Close a file handlefile.size(path)→(int, string)- Get file size in bytesfs.statvfs(path)→(total, free, avail, err)- Filesystem byte counts (POSIXstatvfs) for the fs containingpath.availis space usable by a non-root process (f_bavail) — use it for "how much can I actually write" (end = avail / file_size). Portable Linux/macOS/BSD; Windows returns the error branch.file.delete(path)→string- Delete a file, return error stringfile.exists(path)→int- 1 if exists, 0 otherwise (infallible predicate)
Raw externs: file_open_raw, file_read_all_raw, file_write_raw, file_delete_raw, file_size_raw.
Directory Operations (Go-style)
dir.create(path)→string- Create directory, return error stringdir.delete(path)→string- Delete empty directory, return error stringdir.list(path)→(ptr, string)- List contents (caller mustdir.list_free)dir.exists(path)→int- 1 if exists, 0 otherwisedir.list_count(list)/dir.list_get(list, i)/dir.list_free(list)- DirList accessors
Raw externs: dir_create_raw, dir_delete_raw, dir_list_raw.
Path Utilities
path.join(path1, path2)- Join two path componentspath.dirname(path)- Get directory namepath.basename(path)- Get file namepath.extension(path)- Get file extensionpath.is_absolute(path)- Check if path is absolute
I/O Library
Console Output
The primary I/O functions in Aether are print() and println():
print("Hello, World!\n")
println("Hello, World!") // same, with automatic newline
println("Value: ${x}") // string interpolation
println("Float: ${pi}")
Console Output (infallible)
io.print(str)- Print stringio.print_line(str)- Print string with newlineio.print_int(value)- Print integerio.print_float(value)- Print float
File I/O (Go-style)
All operations that can fail return an error string ("" on success).
content, err = io.read_file("data.txt")
if err != "" { println("failed: ${err}"); return }
werr = io.write_file("output.txt", "hello")
io.read_file(path)→(string, string)- Read entire fileio.write_file(path, content)→string- Write (overwrites)io.append_file(path, content)→string- Append to fileio.delete_file(path)→string- Delete fileio.file_info(path)→(ptr, string)- Get file metadata (caller mustio.file_info_free)io.file_exists(path)→int- 1 if exists, 0 otherwise
Environment variables
io.getenv(name)→string- Returns the value, or null if unset (infallible)io.setenv(name, value)→string- Set env var, return error stringio.unsetenv(name)→string- Unset env var, return error string
Raw externs: io_read_file_raw, io_write_file_raw, io_append_file_raw, io_delete_file_raw, io_file_info_raw, io_setenv_raw, io_unsetenv_raw.
OS / Process Library
Shell execution
os.system(cmd)→intRun a shell command, return exit codeos.exec(cmd)→(string, string)Run a command and capture stdout; returns(output, err)tupleos.getenv(name)→stringRead environment variable; returns null if unset
The shell-execution path passes cmd to /bin/sh -c (cmd.exe /c on Windows), which means quoting, glob expansion, and $VAR interpolation all happen before the child sees the string. Prefer os.run_capture (below) for any input that touches user data, it skips the shell entirely and is binary-safe.
Process spawn (argv-based, no shell)
The argv-based path is the recommended way to launch a child binary. Argv is passed as a list of strings, no shell sits in the middle, paths with spaces / quotes / $-signs are safe, and there's no command-injection surface for user-provided values.
os.run(prog, argv, env)→intSpawnprog, wait for it to finish, return the exit code.argvis alist<ptr>of C strings (element 0 is conventionally the program name; the OS sees this asargv[0]for the child).envis the same shape, ornullto inherit.progis looked up onPATHif it does not contain a slash.os.run_capture(prog, argv, env)→(stdout: string, exit_code: int, stderr: string)Same asos.run, but captures the child's stdout and stderr. The child's three outputs come back as a single tuple. Theexit_codeslot lets callers distinguish "ran cleanly" (exit_code == 0) from "ran but exited non-zero", important for tools likediff3 -m(returns 1 on conflicts),grep(returns 1 on no-match), orgcc(returns non-zero on compile errors), where non-zero is meaningful information rather than a hard failure.
Raw externs (rarely needed directly; the wrappers above are idiomatic):
os_run(prog, argv, env)→intSame asos.run.os_run_capture_raw(prog, argv, env)→stringCaptures stdout only; exit code is discarded. Useos.run_captureinstead unless the exit code is genuinely irrelevant.os_run_capture_status_raw(prog, argv, env)→(string, int, string)The tuple-returning extern thatos.run_capturewraps.
Worked example:
import std.os
import std.list
main() {
argv = list.new()
_ = list.add(argv, "git")
_ = list.add(argv, "rev-parse")
_ = list.add(argv, "HEAD")
stdout, exit_code, stderr = os.run_capture("git", argv, null)
if exit_code != 0 {
println("git failed (${exit_code}): ${stderr}")
return
}
println("HEAD is ${stdout}")
}
Capabilities and Windows portability: os.run / os.run_capture require the --with=os capability when building with --emit=lib (capability-empty by default). Windows MSYS2 / mingw-w64 currently uses POSIX-fallback shims; a native CreateProcessW backend is tracked separately. The function signatures and return shapes are stable across the current Windows fallback and the future native backend.
Synchronous, no streaming. The current API blocks until the child exits and returns the entire captured stdout / stderr. There is no streaming-stdin or incremental-stdout-read path today; if you need to feed input to a child, write your input to a temp file and have the child read from there, or pipe through a shell wrapper.
Argv discovery
aether_args_count()→intNumber of command-line argumentsaether_args_get(index)→stringGet the i-th argument; returns null if out of rangeaether_argv0()→stringPath the OS launched the current process with (argv[0]); returns null beforeaether_args_inithas runos.argv0()→stringConvenience wrapper aroundaether_argv0()that returns""instead of nullos.args_count()→intErgonomic alias foraether_args_count()(#1035)os.args_get(index)→stringErgonomic wrapper: owned copy of argv[index],""when out of range / sealed (never null)
All four raw aether_args_* names also resolve qualified (os.aether_args_count() — the form the language reference shows) since #1035.
Typical use: a tool that needs to find its own binary (to locate sibling helpers next to itself, re-exec with different flags, or print a self-path in a diagnostic) can call os.argv0() and skip the argv-index bookkeeping.
Process replacement
os_execv(prog, argv_list)→intReplace the current process image withprog, passing an explicit argv list.argv_listis alist<ptr>of C strings (element 0 is argv[0] for the new program). On success this call never returns; on failure it returns-1and the current process keeps running.progis looked up onPATHif it does not contain a slash. Not available on Windows, returns-1.
Paired with os.run / os.run_capture (see Process spawn above), this gives Aether programs a full argv-based process-launch surface with no shell in the middle, so paths with spaces, quotes, or $-signs are safe. Stdio is flushed before the exec, so pre-exec diagnostics are not lost.
Example:
import std.os
import std.list
main() {
argv = list.new()
_e1 = list.add(argv, "echo")
_e2 = list.add(argv, "from")
_e3 = list.add(argv, os.argv0())
rc = os_execv("/bin/echo", argv)
// Only reached if exec failed.
println("exec failed: ${rc}")
exit(rc)
}
Math Library
Basic Operations
math.abs_int(x)- Absolute value (int)math.abs_float(x)- Absolute value (float)math.min_int(a, b)- Minimum (int)math.max_int(a, b)- Maximum (int)math.min_float(a, b)- Minimum (float)math.max_float(a, b)- Maximum (float)math.clamp_int(x, min, max)- Clamp value to rangemath.clamp_float(x, min, max)- Clamp value to range
Advanced Math
math.sqrt(x)- Square rootmath.pow(base, exp)- Powermath.sin(x)- Sinemath.cos(x)- Cosinemath.tan(x)- Tangentmath.asin(x)- Arc sinemath.acos(x)- Arc cosinemath.atan(x)- Arc tangentmath.atan2(y, x)- Two-argument arc tangentmath.floor(x)- Floormath.ceil(x)- Ceilingmath.round(x)- Round to nearestmath.log(x)- Natural logarithmmath.log10(x)- Base-10 logarithmmath.exp(x)- Exponential
Random Numbers
math.random_seed(seed)- Set random seedmath.random_int(min, max)- Random int in range [min, max]math.random_float()- Random float in [0.0, 1.0)
JSON Library
Parsing and Serialization
import std.json
main() {
// Parse, Go-style (value, err) tuple.
v, err = json.parse("{\"name\":\"Aether\",\"count\":42}")
if err != "" {
println("parse failed: ${err}")
return
}
// Typed readers are infallible (sentinel 0/0.0 on wrong type).
name_val, _ = json.object_get(v, "name")
name, _ = json.get_string(name_val)
count_val, _ = json.object_get(v, "count")
count = json.get_int(count_val)
println("${name} / ${count}")
// Build values, each create_* allocates a standalone value. Passing
// it to object_set / array_add transfers ownership to the container.
obj = json.create_object()
_ = json.object_set(obj, "x", json.create_number(1.5))
_ = json.object_set(obj, "flag", json.create_bool(1))
arr = json.create_array()
_ = json.array_add(arr, json.create_number(10.0))
_ = json.array_add(arr, json.create_number(20.0))
out, _ = json.stringify(obj)
println(out)
json.free(v)
json.free(obj)
json.free(arr)
}
JSON Functions
Fallible calls return Go-style tuples. The typed readers (get_bool / get_number / get_int) return sentinels (0, 0.0) on wrong-type input so they stay infallible.
json.parse(str)→(ptr, string)parse into a tree. Error is a position-qualified message like"expected ':' at 3:17".json.stringify(value)→(string, string)(output, err).json.free(value)release the value. Safe on both parsed roots (frees the arena) and standalone-created values.json.get_string(value)→(string, string)(text, err); errors ifvalueis not aJSON_STRING.json.object_get(obj, key)→(ptr, string)(child, err). Absent key returns(null, ""), which is distinct from the error case(null, "not an object").json.object_set(obj, key, value)→stringerror string or"".json.array_get(arr, index)→(ptr, string)same shape; out-of-range returns(null, "").json.array_add(arr, value)→stringerror string or"".
Infallible externs (no tuple):
json.type(value)→intreturns one of theJSON_*constants.json.is_null(value)→int.json.get_bool(value)→int0 on wrong type.json.get_number(value)→float0.0 on wrong type (lossy past 2^53).json.get_int(value)→int, clamps to+/-2147483647on overflow.json.get_long(value)→long, the full int64 value exactly. Use this for large integer IDs / byte-counts;get_intclamps andget_numberis lossy.json.object_has(obj, key)→int.json.array_size(arr)→int.json.create_null(),json.create_bool(v),json.create_number(v),json.create_string(s),json.create_array(),json.create_object()→ptrallocate standalone values.json.last_error()→stringthe last parser error on the current thread; redundant withjson.parse's tuple but useful when calling the raw extern directly.
Raw externs (bypass the Go-style wrappers): json_parse_raw, json_parse_raw_n (length-taking, for non-null-terminated input), json_stringify_raw, json_get_string_raw, json_object_get_raw, json_object_set_raw, json_array_get_raw, json_array_add_raw. All documented in stdlib-module-pattern.md.
JSON Type Constants
JSON_NULL= 0JSON_BOOL= 1JSON_NUMBER= 2JSON_STRING= 3JSON_ARRAY= 4JSON_OBJECT= 5
What std.json doesn't do
Coming from Go's json.Unmarshal, Java's Jackson, Python's json.load + dataclasses, or C#'s JsonSerializer, expect to do more by hand:
- No struct ↔ JSON mapping. Aether has no runtime reflection (no
instanceof, noT.GetType(), noreflect.TypeOf), so a library function that takes a struct type and a JSON tree and populates the fields can't exist as a stdlib API. Callers walk the tree by hand:json.object_get(v, "name")thenjson.get_string(...), repeated per field. For tree-shaped or dynamically-shaped JSON the code looks similar to other languages; for struct-shaped JSON it's more verbose. A future codegen step (a--derive-jsonflag on struct definitions, or a build-step macro) could close this gap without runtime reflection, but isn't shipped today. - No annotations or struct tags.
@JsonProperty("user_name"), Go struct tagsjson:"user_name,omitempty", and the like have nothing to attach to without struct-mapping in the first place. - No streaming parse. The whole document is buffered into the arena before the tree is walkable. Documents into the tens of MB are fine; for multi-gigabyte JSON, use a different tool.
- Strict RFC 8259 only: no JSON5, comments, or trailing commas.
- Compact output only on stringify; wrap with a separate prettier if you need one.
- No JSON Schema validation. Validate by hand or build it on top.
- No arbitrary-precision numbers. Numbers are
intordouble; the parser falls through tostrtodfor correctly-rounded IEEE-754 on edge cases (16+ significant digits, huge exponents), but there's noBigDecimal/decimal.Decimalequivalent for financial precision. - Hard-coded depth limit of 256, as DoS protection against deeply nested JSON bombs. Not configurable; rare to hit in practice.
Other structured-data formats
Beyond JSON, XML is the other format with a stdlib module: std.xml (issue #627) provides a pull/SAX reader (xml.parser, xml.next, xml.name, xml.text, xml.attr, driven by the EVENT_START/END/TEXT/EOF/ERROR constants) and an escaping element builder (xml.writer, xml.start, xml.element, xml.finish). Enough for S3 / SOAP-ish / config XML; not in scope are XSD, XPath, namespaces, and DTD validation. There is no DOM tree, so you drive events yourself. The rest have no stdlib parser or codec:
- YAML, INI, and Java-style
.propertieshave no parser. INI and.propertiesare trivial to build onstring.split; for YAML, the runtime is single-language, so Aether projects configure via TOML or hand-rolled formats. - TOML has a parser at
tools/apkg/toml_parser.c, used internally by theaeCLI to readaether.tomlproject files. It isn't exposed asstd.toml. A project needing TOML can copy that parser or shell out to a host-language tool. - CSV has no parser.
string.split(line, ",")covers the no-quoting / no-embedded-commas case; anything more needs a real CSV parser, which isn't shipped. - Protocol Buffers, MessagePack, CBOR, Avro, and Thrift have no codecs. Same reflection-gap reasoning as struct ↔ JSON: without struct introspection there's no automatic encode/decode, so a hand-written codec on top of
tcp.write/tcp.read/aether_string_datais what you'd build.
These are absent because no downstream user has driven the need yet. If you're starting a project that needs YAML config, expect to write a parser, ship a contrib module, or shell out. The structured-data thinking in the stdlib is currently JSON-shaped and HTTP-adjacent; broader format coverage is open territory.
Cryptography Library
Hash digests + Base64 codec. Built on OpenSSL's EVP API. When OpenSSL isn't linked, every wrapper returns ("", "openssl unavailable") rather than crashing.
import std.cryptography
main() {
digest, _ = cryptography.sha256_hex("abc", 3)
b64, _ = cryptography.base64_encode("\x01\x02\x03", 3)
raw, n, _ = cryptography.base64_decode(b64)
}
Hash Functions
cryptography.sha1_hex(data, length)→(string, string)- 40-char lowercase hex digest. Legacy interop (Git, Subversion); prefer SHA-256 for new work.cryptography.sha256_hex(data, length)→(string, string)- 64-char lowercase hex digest.cryptography.hash_hex(algo, data, length)→(string, string)- Algorithm-by-name dispatcher.algois"sha1","sha256", or any nameEVP_get_digestbyname()recognizes ("sha384","sha512","sha3-256", ...). Returns("", "unknown algorithm")for unrecognized names.cryptography.hash_supported(algo)→int-1if this build can computealgo,0otherwise. Always succeeds. Use to validate user-supplied algorithm names before callinghash_hex.cryptography.md4_hex(data, length)/md5_hex(data, length)→(string, string)- 32-char hex digest. Legacy interop only (Content-MD5, ETag, zsync), NOT collision-resistant.cryptography.sha1_bytes(data, length)/sha256_bytes(data, length)/md4_bytes(data, length)/md5_bytes(data, length)→(string, int, string)- Raw-bytes digest,(bytes, length, err);bytespreserves embedded NULs.cryptography.hash_bytes(algo, data, length)→(string, int, string)- Algorithm-by-name binary digest.
length is explicit so binary payloads with embedded NULs survive. data may be either a plain string literal or an AetherString from fs.read_binary the runtime unwraps automatically.
HMAC, Random, and Streaming Digests
cryptography.hmac_sha256_hex(key, key_len, msg, msg_len)→(string, string)- HMAC-SHA256 hex digest (RFC 2104 / FIPS 198-1).cryptography.hmac_sha256_bytes(key, key_len, msg, msg_len)→(string, int, string)- Raw 32-byte HMAC for chained key derivation (SigV4 / HKDF-shaped flows).cryptography.random_bytes(n)→(string, int, string)-nCSPRNG bytes from the OS (getrandom/arc4random_buf).cryptography.random_hex(n)→(string, string)-nrandom bytes as2*nhex chars.cryptography.random_base64(n)→(string, string)-nrandom bytes as unpadded Base64.cryptography.digest_new(algo)→(ptr, string)- Open a streaming digest context ("md5","sha256", ...).cryptography.digest_update(ctx, data, length)→(int, string)- Feed bytes incrementally; does not freectx.cryptography.digest_final_hex(ctx)→(string, string)/digest_final_bytes(ctx)→(string, int, string)- Finalize; both FREEctx.cryptography.digest_free(ctx)- Abandon a context without finalizing (NULL-safe).
Base64 (RFC 4648 §4 standard alphabet)
cryptography.base64_encode(data, length)→(string, string)- Encodelengthbytes, unpadded output.cryptography.base64_encode_padded(data, length)→(string, string)- Encodelengthbytes, with=padding to a multiple of 4. For wire formats (auth headers, JSON-encoded blobs) that require padded output.cryptography.base64_decode(b64)→(string, int, string)- Decode. Returns(bytes, byte_count, "")on success,bytesis an AetherString preserving embedded NULs. Accepts both padded and unpadded input.
What's not in std.cryptography
The public-key and cipher families (RSA, ECDSA, Ed25519, X25519, AES, ChaCha20-Poly1305, …) live under std.cryptography as explicitly-imported sub-modules (std.cryptography.rsa, std.cryptography.x25519, …) — pure-Aether, no OpenSSL. URL-safe Base64 (RFC 4648 §5) and constant-time comparison remain out of the top-level module. See stdlib-reference.md §"What std.cryptography doesn't do" for the rationale.
Networking Library
HTTP Client (Go-style)
import std.http
main() {
body, err = http.get("http://example.com")
if err != "" {
println("failed: ${err}")
return
}
println(body)
}
See examples/stdlib/http-client.ae for a runnable version.
HTTP Client Functions
http.get(url)→(string, string)- HTTP GET, auto-frees responsehttp.post(url, body, content_type)→(string, string)- HTTP POSThttp.put(url, body, content_type)→(string, string)- HTTP PUThttp.delete(url)→(string, string)- HTTP DELETE
All wrappers return ("", err) for transport failures and for any non-2xx HTTP status. If you need status codes or headers, use the raw extern + accessor pattern:
response = http.get_raw(url)
status = http.response_status(response)
body = http.response_body(response)
http.response_free(response)
Raw externs: http_get_raw, http_post_raw, http_put_raw, http_delete_raw.
Response accessors (used with the raw API):
http.response_status(response)- Read HTTP status code (0 on transport failure)http.response_body(response)- Read response body as stringhttp.response_headers(response)- Read response headers as stringhttp.response_error(response)- Read transport error stringhttp.response_ok(response)- 1 if transport succeeded AND status is 2xx, else 0http.response_free(response)- Free response
HTTP Server
import std.http
main() {
server = http.server_create(8080)
berr = http.server_bind(server, "127.0.0.1", 8080)
if berr != "" {
println("bind failed: ${berr}")
return
}
serr = http.server_start(server) // Blocks
if serr != "" {
println("start failed: ${serr}")
}
http.server_free(server)
}
Server Functions
http.server_create(port)- Create server (never fails)http.server_bind(server, host, port)→string- Bind to address, return error stringhttp.server_start(server)→string- Start serving (blocking), return error stringhttp.server_stop(server)- Stop serverhttp.server_free(server)- Free serverhttp.server_get(server, path, handler, data)- Register GET handlerhttp.server_post(server, path, handler, data)- Register POST handlerhttp.response_json(res, json)- Send JSON responsehttp.response_set_status(res, code)- Set status codehttp.response_set_header(res, name, value)- Set header
Raw externs: http_server_bind_raw, http_server_start_raw.
Server Configuration
http.server_set_tls(server, cert_path, key_path)→string- HTTPS with PEM cert + keyhttp.server_set_keepalive(server, enable, max_requests, idle_timeout)→string- HTTP/1.1 keep-alive with aDurationidle timeouthttp.server_set_h2(server, max_concurrent_streams)→string- HTTP/2 (h2 + h2c + ALPN).0uses libnghttp2's default (100). Returns error string when libnghttp2 isn't linked.http.server_set_h2_concurrent_dispatch(server, worker_count)→string- Routes h2 stream handlers onto the sharedstd.workerthread pool, sized toworker_count.worker_count > 0lets streams across all h2 connections execute their handlers in parallel;0(default) keeps dispatch sequential. POSIX-only; on Windows the call is a silent no-op.http.server_shutdown_graceful(server, timeout)→string- Stop accepting, drain in-flight for up to aDuration, exit. h2 sessions emit GOAWAY so peers don't start new streams.http.server_set_health_probes(server, live_path, ready_path, ready_check, ud)→string- Built-in/healthz(always 200) +/readyz(200 only when readiness check returns 1)http.server_set_access_log(server, format, output_path)→string-"combined"or"json"to a file path /"-"(stderr) /""(disable)http.server_set_metrics(server, endpoint)→string- Prometheus-compatible counters/histograms (default/metrics)
HTTP Middleware (std.http.middleware)
Pre-handler middleware (run before route dispatch; can short-circuit with a populated response):
middleware.use_cors(server, allow_origin, allow_methods, allow_headers, allow_credentials, max_age_seconds)→stringmiddleware.use_basic_auth(server, realm, verify_cb, ud)→string- HTTP Basic (RFC 7617). Verifier receives decoded(username, password).middleware.use_bearer_auth(server, realm, verify_cb, ud)→string- Bearer token (RFC 6750). Verifier receives raw token; bad credentials getWWW-Authenticate: Bearer ... error="invalid_token".middleware.use_session_auth(server, cookie_name, redirect_url, verify_cb, ud)→string- Reads a named cookie; on failure 401 (whenredirect_urlis empty) or 302.middleware.use_rate_limit(server, max_requests, window_ms)→string- Token bucket per client IP.middleware.use_vhost(server, hosts_csv)→string- Host-header gate; unknown hosts get 404.middleware.use_real_ip(server, header_name)→string- ReadsX-Forwarded-For(or configured header), takes leftmost IP, addsX-Real-IP. Only safe behind a trusted proxy that strips client-supplied XFF.middleware.use_static_files(server, url_prefix, root)→string- Mount a directory;..traversal blocked.middleware.use_rewrite(server, opts)→string- Prefix rewrite rules; build viamiddleware.rewrite_add_rule(opts, from, to).
Response transformers (run after the route handler emits the response):
middleware.use_gzip(server, min_size, level)→string- Gzip body when client sendsAccept-Encoding: gzipand body is at leastmin_sizebytes.middleware.use_error_pages(server, opts)→string- Custom error-status bodies; build viamiddleware.error_pages_register(opts, status, body, content_type).
HTTP Client Builder (std.http.client)
Builder-shaped requests with full response access. The http.get / http.post / http.put / http.delete one-liners above are good for "no auth, JSON in, 200 means good" calls; reach for std.http.client when you need custom request headers, response-header capture, status discrimination, per-request timeouts, or methods other than the four common verbs (PROPFIND, PATCH, custom RPC verbs all work). Method is an arbitrary string. Non-2xx is not an error, the caller checks response_status.
import std.http.client
main() {
req = client.request("GET", "https://api.example.com/users/42")
client.set_header(req, "Authorization", "Bearer abc123")
client.set_timeout(req, 30s)
resp, err = client.send_request(req)
client.request_free(req)
if err != "" { return }
status = client.response_status(resp)
body = client.response_body(resp)
client.response_free(resp)
}
Builder + send:
client.request(method, url)→ptr- Build a request handleclient.set_header(req, name, value)→string- Append a request headerclient.set_body(req, body, length, content_type)→string- Set request body (length explicit for binary safety)client.set_timeout(req, timeout)→string-Durationper-request timeout (0ns= block forever)client.set_follow_redirects(req, max_hops)→string- Follow up tomax_hopsredirects (0= don't follow, the default)client.send_request(req)→(ptr, string)- Fire it;(resp, "")on success,(null, err)on transport failureclient.request_free(req)- Free the request handle
TLS + proxy (per request):
client.set_insecure(req, on)→string-1skips TLS peer + hostname verification for this request only (curl -k/--no-check-certificate); relaxed per-connection, never on the sharedSSL_CTX. Default0(verify). Use only against hosts you trust out-of-band.client.set_cafile(req, path)→string- pin a custom CA for this request: verify the peer against the PEM bundle atpathinstead of the system trust store, keeping peer + hostname verification on. Strictly stronger thanset_insecure— for M2M calls to a host with a private/self-signed CA you've couriered out-of-band (e.g. a Proxmox VE API'spve-root-ca.pem). Per-connection (never touches the sharedSSL_CTX);""clears the pin. A cert the pinned CA doesn't cover fails the handshake (fails closed, never open).client.use_env_proxy(req, on)→string-1follows$HTTP_PROXY/$HTTPS_PROXY/$NO_PROXY. Off by default — the client does not honour env proxies unless you opt in (the hardened inverse of the httpoxy/CVE-2016-5385 default-follow). Guarded: the CGI-injectable uppercaseHTTP_PROXYis refused under$REQUEST_METHOD/$GATEWAY_INTERFACE, and a proxy at a loopback/link-local IP is rejected (SSRF).client.use_http_proxy(req, "http://host:port")→string- Pin an explicit forward proxy; env is ignored entirely (empty url = direct). A team-controlled proxy is immune to whatever the shell/CI set.client.ignore_http_proxy(req)→string- Force a direct connection regardless of env / any set proxy (determinism escape hatch, e.g. VCR record mode). Precedence: ignore > explicit > env > direct.
Response accessors:
client.response_status(resp)→intclient.response_body(resp)→string(binary-safe AetherString)client.response_header(resp, name)→string(case-insensitive)client.response_headers(resp)→string(raw header block)client.response_error(resp)→stringclient.response_free(resp)- Free the response
Sugar wrappers (pure Aether on top of the builder):
client.get_with_headers(url, header_pairs)→(string, int, string)client.post_with_status(url, body, content_type)→(string, int, string)client.post_json(url, value)→(ptr, string)- Marshal value viastd.json, set Content-Type + Acceptclient.response_body_json(resp)→(ptr, string)-response_body+json.parseround-trip
See tests/integration/test_http_client_v2.ae for ten worked examples (header round-trip, status discrimination, binary body, timeout, transport failure, JSON sugar, malformed-JSON parse failure); the Standard Library Reference covers the design choices (arbitrary-string method, non-2xx-is-not-an-error, send_request naming). Streaming response bodies for large downloads are tracked in #1004.
HTTP Reverse Proxy (std.http.proxy)
nginx-class outbound HTTP forwarding for the Aether server. Five load-balancing algorithms, active health checks, in-memory LRU response cache (Vary-aware), per-upstream circuit breaker, idempotent retry with proxy_next_upstream semantics, per-upstream token-bucket rate limit, active drain, W3C Trace-Context propagation, and Prometheus 0.0.4 metrics.
import std.http
import std.http.proxy
main() {
server = http.server_create(8080)
pool = proxy.upstream_pool_new("weighted_rr", 30, 0, 0)
proxy.upstream_add(pool, "http://10.0.0.1:8080", 3)
proxy.upstream_add(pool, "http://10.0.0.2:8080", 1)
proxy.health_checks_enable(pool, "/health", 200, 5000, 1000, 2, 3)
proxy.breaker_configure(pool, 5, 30000, 1)
cache = proxy.cache_new(1000, 65536, 60, "method_url_vary")
opts = proxy.opts_new()
proxy.opts_bind_cache(opts, cache)
proxy.opts_set_retry_policy(opts, 3, 100)
proxy.mount(server, "/api", pool, opts)
http.server_start(server)
}
Pool + LB: proxy.upstream_pool_new(lb_algo, request_timeout_sec, dial_timeout_ms, max_inflight_per_up) -> ptr (algos: round_robin, least_conn, ip_hash, weighted_rr, cookie_hash), upstream_pool_free, upstream_add(pool, base_url, weight), upstream_remove, upstream_drain / upstream_undrain, pool_set_cookie_name, rate_limit_set(pool, max_rps, burst).
Health: proxy.health_checks_enable(pool, probe_path, expect_status, interval_ms, timeout_ms, healthy_threshold, unhealthy_threshold).
Breaker: proxy.breaker_configure(pool, failure_threshold, open_duration_ms, half_open_max).
Cache: proxy.cache_new(max_entries, max_body_bytes, default_ttl_sec, key_strategy) -> ptr (strategies: url, method_url, method_url_vary).
Per-mount opts: opts_new, opts_set_strip_prefix, opts_set_preserve_host, opts_set_xforwarded(opts, xff, xfp, xfh), opts_bind_cache, opts_set_body_cap, opts_set_retry_policy(opts, max_retries, backoff_base_ms), opts_set_trace_inject(opts, on).
Install: proxy.mount(server, path_prefix, pool, opts) or single-upstream proxy.mount_simple(server, path_prefix, upstream_url, request_timeout_sec).
Metrics: proxy.pool_metrics_text(pool) -> string returns Prometheus 0.0.4 exposition (per-upstream counters by status class, transport_errors, timeouts, retries, latency_ms_sum/count + gauges for inflight/healthy/breaker_state/draining; pool-level cache_hits/misses/revalidations/503_no_upstream).
Full reference: docs/http-reverse-proxy.md.
HTTP Record/Replay (VCR), moved out of the stdlib
The Servirtium record/replay engine that used to ship as std.http.server.vcr has been lifted into its own repository, servirtium-vcr, which is now its authoritative home (alongside its language bindings). It is no longer part of the Aether stdlib. See docs/http-vcr.md.
TCP Sockets (Go-style)
Note:sendandreceiveare reserved actor keywords in Aether, so the TCP wrappers usewrite/readinstead. The raw externs retain thesend_raw/receive_rawnaming.
tcp.connect(host, port)→(ptr, string)- Connect, return(socket, err)tcp.write(sock, data)→(int, string)- Text-shaped write, return(bytes, err)tcp.write_n(sock, data, length)→(int, string)- Length-aware write for binary payloadstcp.read(sock, max_bytes)→(string, string)- Text-shaped read, return(data, err)tcp.read_n(sock, max_bytes)→(string, int, string)- Binary-safe read, return(bytes, length, err)tcp.poll(sock, timeout_ms)→(bool, string)- Wait for readability.(true, "")= a followingread_nwon't block,(false, "")= timeout,(false, err)= closed/failed.timeout_ms:-1blocks,0polls,>0waits that many ms.tcp.poll2(a, b, timeout_ms)→(bool, bool, string)- Wait on two sockets at once (the full-duplex relay primitive); each flag is true when that side is readable, both may be true.tcp.listen(port)→(ptr, string)- Create listening sockettcp.accept(server)→(ptr, string)- Accept connectiontcp.close(sock)- Close socket (infallible)tcp.server_close(server)- Close server socket
Timeout vs. close (#1092). A quiet-but-alive peer (recv-timeout / would-block) is not a close:tcp.read_nreturns the distinct"timeout"error and leaves the socket connected so you can retry ortcp.pollit. Only an orderly FIN or a hard error returns"connection closed or receive failed". A full-duplex relay must branch on the"timeout"sentinel rather than collapsing all non-empty errors into "closed."
Raw externs: tcp_connect_raw, tcp_send_raw, tcp_send_n_raw, tcp_receive_raw, tcp_receive_n_raw, tcp_listen_raw, tcp_accept_raw, tcp_poll_raw, tcp_poll2_raw.
Reactor-Pattern Async I/O (await_io)
Aether's scheduler has a per-core I/O reactor (epoll on Linux, kqueue on macOS/BSD, poll() elsewhere) that can suspend an actor on a file descriptor without blocking any OS thread. When the fd becomes ready, the scheduler delivers an IoReady message to the actor's mailbox and resumes it on any available core.
import std.net
message IoReady { fd: int, events: int }
message Connection { fd: int }
actor Worker {
receive {
Connection(fd) -> {
req = ae_http_recv(fd)
http_response_json(res, "{\"hello\":\"world\"}")
net.await_io(fd) // suspends, zero CPU until data arrives
}
IoReady(fd, events) -> {
// Resumed here when fd is readable again
req = ae_http_recv(fd)
http_response_json(res, "{\"hello\":\"world\"}")
net.await_io(fd)
}
}
}
The IoReady message name is reserved. The Aether message registry assigns it the ID that the runtime scheduler uses for I/O readiness notifications, so any actor that defines message IoReady { fd: int, events: int } will receive scheduler-delivered events in that arm.
Functions:
net.await_io(fd)→stringRegisterfdwith the current core's I/O poller and mark the calling actor as waiting. Returns""on success, error string otherwise (invalid fd, no active actor context, or scheduler refused the registration). One-shot: the fd is automatically unregistered after theIoReadydelivery.net.ae_io_cancel(fd)Abandon a priorawait_iowithout waiting for the message. Rare; the one-shot policy makes this unnecessary in most flows.
Performance note: PR #140 demonstrated the raw reactor pattern delivering substantially higher HTTP throughput than a blocking keep-alive worker. await_io is the Aether-language surface over that same machinery, rerun the HTTP benchmark on your target host to get a current figure for your environment.
Collections Library
ArrayList
import std.list
main() {
mylist = list.new()
defer list.free(mylist)
list.add(mylist, some_ptr)
item = list.get(mylist, 0)
size = list.size(mylist)
}
ArrayList Functions
list.new()- Create new list (never fails)list.add(list, item)→string- Append item, return error string (non-empty on resize/OOM)list.get(list, index)- Get item (returns null for out-of-bounds)list.set(list, index, item)- Set item at indexlist.remove(list, index)- Remove item at indexlist.size(list)- Get sizelist.clear(list)- Clear all itemslist.free(list)- Free list
Raw extern: list_add_raw (returns 1/0).
HashMap
import std.map
main() {
mymap = map.new()
defer map.free(mymap)
map.put(mymap, "name", some_ptr)
result = map.get(mymap, "name")
exists = map.has(mymap, "name")
}
HashMap Functions
map.new()- Create new map (never fails)map.put(map, key, value)→string- Put key-value pair, return error string (non-empty on resize/OOM)map.get(map, key)- Get value by key (returns null if missing)map.remove(map, key)- Remove keymap.has(map, key)- Check if key existsmap.size(map)- Get number of entriesmap.clear(map)- Clear all entriesmap.free(map)- Free map
Raw extern: map_put_raw (returns 1/0).
Set
Unique strings, backed by the std.map hash table. Items are copied on insert. O(1) average lookup.
import std.set
visited = set.new()
set.add(visited, "/index") // true
set.add(visited, "/index") // false, already present
set.new()→ptr- Create a new set (null on allocation failure)set.add(set, item)→bool- True if added, false if already present or the insert failedset.contains(set, item)→bool- Membership testset.remove(set, item)- Drop an item; absent items are ignoredset.size(set)→int- Number of unique itemsset.clear(set)- Drop every item, keeping the set usableset.free(set)- Release the setset.items(set)→(ptr, string)- Snapshot of the items, unspecified order; release withset.items_freeset.items_free(items)- Release a snapshot
Null-set calls are safe (size 0, contains false). Raw externs are the aether_set_* entry points.
PriorityQueue
Binary heap over (priority, item) pairs; lowest priority pops first. Push/pop are O(log n), peek/size O(1). The queue does not own items, it never frees them.
import std.pqueue
jobs = pqueue.new()
pqueue.push(jobs, 5, "page on-call")
next = pqueue.pop(jobs)
pqueue.new()→ptr- Create a new queue (null on allocation failure)pqueue.push(pq, priority, item)→bool- Enqueue; false on a null queue or allocation failurepqueue.pop(pq)→ptr- Remove and return the lowest-priority item, null when emptypqueue.peek(pq)→ptr- Next item without removing it, null when emptypqueue.peek_priority(pq)→long- Priority of the next item (0 when empty)pqueue.size(pq)→int- Number of queued entriespqueue.is_empty(pq)→bool- True when nothing is queuedpqueue.clear(pq)- Drop every entry, keeping the queue usablepqueue.free(pq)- Release the queue
Null-queue calls are safe (size 0, pop/peek null). Raw externs are the aether_pqueue_* entry points.
Logging Library
import std.log
main() {
err = log.init("app.log", 0) // 0 = DEBUG level
if err != "" {
println("cannot open log file: ${err}")
// falls back to stderr automatically
}
log.write(0, "Debug message")
log.write(1, "Info message")
log.write(2, "Warning message")
log.write(3, "Error message")
log.print_stats()
log.shutdown()
}
Logging Functions
log.init(filename, level)→string- Initialize logging, return error string if the log file could not be opened (logging still falls back to stderr)log.shutdown()- Shutdown logginglog.write(level, message)- Write a log message at the given levellog.set_level(level)- Set minimum levellog.set_colors(enabled)- Enable/disable colored output (1/0)log.set_timestamps(enabled)- Enable/disable timestamps (1/0)log.get_stats()- Get logging statisticslog.print_stats()- Print logging statistics
Raw extern: log_init_raw (returns 1/0).
Log Levels
0= DEBUG1= INFO2= WARN3= ERROR4= FATAL
Concurrency Functions
Actor Management
spawn(ActorName())- Create a new actor instance
Synchronization
wait_for_idle()- Block until all actors finish processingsleep(milliseconds)- Pause execution
Example
message Task { id: int }
actor Worker {
state completed = 0
receive {
Task(id) -> {
completed = completed + 1
}
}
}
main() {
w = spawn(Worker())
w ! Task { id: 1 }
w ! Task { id: 2 }
wait_for_idle()
print("Completed: ")
print(w.completed)
print("\n")
}
Memory Management
Aether uses manual memory management with defer as the primary tool.
defer
Use defer immediately after allocation to ensure cleanup at scope exit:
import std.list
import std.string
main() {
mylist = list.new()
defer list.free(mylist)
s = string.new("hello")
defer string.free(s)
// ... use mylist and s ...
// Automatically freed when scope exits
}
Guidelines
defer type.free(x), primary cleanup pattern for all allocations- Stack allocations, freed automatically (no
deferneeded) - Actors, managed by the runtime
- Managed strings, reference-counted internally; use
string.free()(alias forstring.release()) string.retain(str), advanced: increment reference count when sharing ownership across C callbacks
Best Practices
- Use
importfor stdlib - Cleaner thanextern - Use
print()for output - Simple and reliable - Free resources - Use
defer type.free(x)after allocation, or explicit.free()calls - Enable bounds checking in debug - Catches array errors
- Use actors for concurrency - Safer than manual threading
Example: Complete Program
import std.file
message Increment { amount: int }
actor Counter {
state count = 0
receive {
Increment(amount) -> {
count = count + amount
}
}
}
main() {
print("Aether Runtime Example\n")
if (file.exists("README.md") == 1) {
print("README.md found!\n")
}
counter = spawn(Counter())
counter ! Increment { amount: 1 }
counter ! Increment { amount: 1 }
wait_for_idle()
print("Final count: ")
print(counter.count)
print("\n")
}