C interop
Calling C, and the safety rules that come with it.
C interop
slang already transpiles to C and shells out to cc, so calling into existing C libraries is a thin layer on top of that, not a new ecosystem: declare the C function's signature, tell the linker which library to pull in, and call it like any other function.
link "sqlite3"; // -> '-lsqlite3' on the final cc invocation
extern fn sqlite3_libversion() -> str;
println(sqlite3_libversion());extern fn name(params) -> ret;declares a C function with no body — it calls the real, unmangled C symbol directly.int,i8..u64,f32,float,bool, andstralready share their C representation, so they marshal for free.bytesdoes not auto-decay (it is a boxed struct internally); passbytes_ptr(b)andlen(b)as two separaterawptr/i32arguments instead of inventing implicit multi-argument expansion for one type.rawptris an opaque foreign pointer (void *) for handles a C library owns, likesqlite3*orFILE*. It can be passed around and compared againstnullptr, nothing else — no arithmetic, no field access, no dereference. Arawptris never GC-owned: if a C library allocated it, free it through anotherextern fn, not by letting it go out of scope.link "name";is a top-level directive (parsed likeimport) that adds-lnameto theccinvocation. Non-default search paths go throughLIBRARY_PATH/CPATH, whichccalready honors — no separate slangc flag for that.- Only types with an unambiguous C representation may cross an
extern fnboundary: numeric types,bool,str,bytes,rawptr, andptr[T]of those types. GC'd containers (opt,result,map, structs, arrays) are rejected at compile time — their internal layout isn't something arbitrary C code should ever see.
C++ is out of scope for the compiler itself. There's no name-mangling/ABI support planned. Wrap the C++ library in your own extern "C" shim (catching every exception at that boundary — an uncaught C++ exception unwinding into C is undefined behavior) and consume the shim exactly like any other C library above.
Safety notes:
- The collector is precise for slang values rooted at safepoints. A slang value whose only remaining reference lives in memory the GC cannot scan (possible with some C libraries) can be collected while C still holds it. Keep a live slang-side reference for the duration of any call that retains a pointer beyond that call.
- Callback function pointers — C calling back into slang — aren't supported yet.
- Deep C libraries and the task stack. A task's stack starts at 8KB and grows only at slang checkpoints, which C code has none of. A C function that recurses or holds large locals can therefore run off the end of the stack buffer and corrupt the heap — a silent
abort()frommalloc, not a clean crash. The compiler-provided packages that wrap deep libraries grow the stack up front (sl_rt_need_stack, used bynet.tls_*for OpenSSL and bysqlfor SQLite); anextern fninto a comparably deep library has no such protection, so keep C-side recursion and stack buffers small.
See tests/ffi/ for a complete example: a small hand-written C fixture library (lib.c) built as a static archive, linked and called from a slang program exercising extern fn, link, rawptr, bytes_ptr, and nullptr.
Packages (Go/Odin style)
A package is a directory: every .sl file inside it is compiled together into one shared namespace, as if concatenated. Import paths resolve to a directory next to the importer, then a native package, then stdlib/<path>, then a pin in slang.project. Only the exact path names a native package ("json" is the built-in; "lib/json" is a directory), and a directory shadows a native package only if it holds .sl files, so a json/ folder of fixtures does not.
import "geometry"; // binds the name "geometry" in this file's scope
import "a/b/util"; // nested paths bind as "util"
import "geometry" as geo; // optional alias; call as geo.area(...)
println(geometry.area(3.0, 4.0)); // qualified access
println(util.format(x));Exports are explicit. Only declarations marked with pub are visible to importers; everything else is private to its package:
// geometry/shapes.sl
pub fn area(w: float, h: float) -> float { ... } // exported
fn scale(v: float) -> float { ... } // private
// geometry/consts.sl
pub let pi = 3.14159; // exported package constant
let secret = 42; // private package globalRules:
- Accessing a non-
pubmember from outside is a compile error. - Within a package, members are used unqualified:
area(1, 2). - In an imported package, top-level
letbecomes a package global; its initializer must be a constant literal. - The entry point is the file you pass to
slangc; its directory is the main package, and its top-level statements run in order inmain(). Other files of the main package share its namespace. - Duplicate names within a package, duplicate import bindings, and import cycles are all compile errors.
- Symbols are mangled per package (
sl_<pkg>_<name>), so different packages can safely use the same names. - A package's name is its directory's name made an identifier: any character other than a letter, digit or
_becomes_, and a leading digit gets ap_prefix (app.v2→app_v2,9lib→p_9lib). The import binding is still the path's last element, so a directory whose name is not an identifier needs an alias:import "lib.v2" as lib;. - Different directories are always different packages, even with the same name:
import "a/util";andimport "b/util" as butil;are two packages, and two libraries can each have their ownutil. When names clash, diagnostics show the later one with a suffix (util_2); native packages and the entry package keep theirs.
See examples/pkgdemo/ for a complete multi-package project.
External packages are pinned in slang.project (walked up from the entry file). Imports stay short. slang.lock holds content hashes and is written by slangc get, never by hand. The file needs a name and a version line as well as its pins; slangc new writes both.
name myserver
version 0.1.0
pkg foo git https://github.com/dolphlabs/foo tag v0.1.0
pkg bar git https://github.com/dolphlabs/bar tag v0.2.0 dir srcimport "foo";
import "bar";dir <subdir> points a pin at the package inside the repository, for a library that also ships examples, docs and its own tests: only that directory is compiled into your program, and nothing else in the repository can affect your build. The subdirectory is a relative path within the clone — .., absolute paths and empty segments are refused — and the lock hashes the package's complete included tree, not just the selected source directory.
A package author can keep non-runtime directories out of every consumer's cache by listing them in that package repository's own slang.project:
name foo
version 1.2.0
exclude bench
exclude examples
exclude testsEach exclude is one literal repository-relative directory; paths with .., absolute paths, empty segments, or glob syntax are rejected. Do not exclude the package directory selected by a consumer's dir; slangc get refuses that pin. A directory inside it may be excluded (test fixtures, say) as long as the package never imports it: an import that lands in an excluded directory is a compile error naming the exclude line, because otherwise import "json" from a package whose own json/ was excluded would quietly compile the standard json instead. slangc get uses a shallow partial clone and sparse checkout so a Git server that supports partial clones need not send excluded file contents. If it does not, slang still omits those directories and Git metadata from the local cache, but the first transfer may include them. The lock hash covers every retained file, so editing included source is detected before compilation. Projects without exclude keep the existing full-tree behavior.
slangc get clones each pkg line into $SLANG_CACHE/pkg/<name>/<hash> (~/.cache/slang if unset), links $SLANG_CACHE/pkg/<name>/<tag> to it (a / in the tag becomes _), and prints one line per package with that typeable path — zokor v0.1.0: /home/you/.cache/slang/pkg/zokor/v0.1.0/src — for reading a package's source or docs. Builds never go through the link; they use the lock's hash. If a fetched package has its own slang.project, those pins are fetched too and recorded only in slang.lock. Compile does not hit the network. A missing lock, missing cache, or hash mismatch is an error. Same short name at two git/tag pairs is an error.