So provides several tools for easy C interop.
Includes • Linking • Extern declarations • Inlining • Promoting • Qualifiers • Embeds • Raw C • Helpers
Include a C header file. By default, so:include emits in the .h file, making the header visible to consumers:
//so:include <stdint.h>Use so:include.c when the include is purely an implementation detail that should only appear in the .c file:
//so:include.c "internal_helper.h"When a package uses a C library that must be linked explicitly, declare it with so:link. The name is the library as passed to the linker's -l flag, without the prefix:
//so:include <pthread.h>
//so:link pthreadso build, so run, and so test collect the so:link libraries from every transpiled package, deduplicate them, and pass them to the C compiler (-lpthread above) after your LDFLAGS.
The standard library already uses so:link for its packages. For example, importing so/math automatically links -lm, and importing so/sync or so/conc automatically links -lpthread.
The flags are always emitted. On platforms where a library is already part of libc (for example pthreads and libm on macOS) the extra -l is a harmless no-op.
Declare an external C type (excluded from emission) with so:extern:
//so:extern
type Account struct {
name string
balance int64
flags []uint8
}Declare an external C function:
//so:extern
func dec_balance(acc *Account, amount int64) int64 {
return 42 // for testing
}When calling extern functions, string and []T arguments are automatically decayed to their C equivalents: string literals become raw C strings ("hello"), string values become char* (.ptr), and slices become raw pointers (.ptr). This means C macros don't need to extract .ptr themselves:
//so:extern
func fopen(path string, mode string) *File { return nil }
// Go call:
f := fopen("/tmp/test.txt", "w")
// Generated C:
// fopen("/tmp/test.txt", "w")
// not fopen(so_str("/tmp/test.txt"), so_str("w"))The so:extern directive supports two optional parameters: a C name override and the nodecay flag.
Methods can be extern too.
Name override specifies the C name to use instead of the default package-prefixed name. Useful for extern types that must match a C header:
//so:extern Account
type Account struct {
name string
balance int64
}
// Uses "Account" in C instead of "main_Account"Nodecay skips the automatic decay of So types (so_String, so_Slice) to raw C types. Use this for C functions that are "So-aware" and accept So types directly:
//so:extern nodecay
func set_name(acc *Account, name string)
// Generated C passes so_String directly:
// set_name(&acc, name)
// not set_name(&acc, so_cstr(name))Both options can be combined:
//so:extern MyFunc nodecay
func MyFunc(s string)A c:"..." struct tag overrides the C name of a single field. This is needed when a C struct has a field whose name is a Go keyword, such as type:
//so:extern SDL_CommonEvent
type SDL_CommonEvent struct {
etype uint32 `c:"type"`
timestamp uint64
}So sees the field as etype; the generated C uses type everywhere, including field accesses in packages that import the struct. The tag value must be a valid C identifier that is not a C keyword, and it may not collide with another field's C name in the same struct.
The tag is honored only on the fields of a named struct type, not on anonymous or function-local structs. It works on any named struct, not just extern ones, but its main use is matching an external C layout.
Writing extern declarations by hand is slow for a large C library. sobind reads .h files and writes a Go source file with so:extern stubs for structs, unions, constants, function pointer typedefs, and function declarations:
go install solod.dev/sobind@latest
sobind -pkg main -o sqlite3.go sqlite3.h
sobind -I . -o sdl3.go SDL3
Given a directory, sobind processes every .h file in it. Use -I to add an include search directory.
sobind might map some declarations incorrectly. Treat the generated file as a starting point, not as the final binding: read it, and correct the types that sobind could not map.
Force a function to be emitted as static inline in the header file using //so:inline. This is useful for small, frequently used functions when the compiler won't inline them automatically:
//so:inline
func add(a, b int) int {
return a + b
}The function body is emitted directly in the .h file and skipped from the .c file. Works with both functions and methods.
By default an unexported symbol (lowercase name) stays in the .c file with its bare name. You can promote it into the header with //so:promote, which also gives it the package prefix:
//so:promote
type counter struct { val int }
//so:promote
func newCounter() counter { ... }
//so:promote
func (e *counter) inc() { ... }// pkg.h
typedef struct pkg_counter { so_int val; } pkg_counter;
pkg_counter pkg_newCounter(void);
void pkg_counter_inc(void* self);Types are emitted in full; functions and methods get a header prototype while their body stays in the .c file; variables become extern; constants are emitted with their value. A method's C name comes from its receiver type, so an so:promote method requires the receiver type to be exported or so:promote too; otherwise it is rejected.
This is needed when an exported so:inline function (whose body lives in the header) calls an unexported helper, or when an exported type has a field of an unexported type:
type Stats struct { c counter }
//so:inline
func NewStats() Stats {
return Stats{c: newCounter()}
}Without so:promote, the header would reference a name it never declares, so the compiler rejects the declaration. The alternative (exporting the helper) pollutes the public API; so:promote keeps it out of the Go API while still making the C declaration visible.
so:promote works on types, functions, methods, vars, and consts. It is rejected on exported declarations (already in the header, so redundant) and cannot combine with so:inline (which already emits the body in the header).
Mark a package-level variable as volatile using //so:volatile:
//so:volatile
var counter intOnly allowed on var declarations.
Mark a package-level variable as thread-local using //so:thread_local. Uses C11 _Thread_local:
//so:thread_local
var perThread intCan be combined with //so:volatile:
//so:volatile
//so:thread_local
var flags intOnly allowed on var declarations.
Add GCC/Clang __attribute__ annotations using //so:attr. The text after so:attr is used as the attribute value:
//so:attr packed
type header struct {
version byte
length int
}Multiple //so:attr lines on the same declaration are combined:
//so:attr packed
//so:attr aligned(16)
type aligned struct {
x int
}Allowed on var, const, type, and func declarations.
Embed C files directly into the generated output using //so:embed:
//so:embed main.h
var main_h string
//so:embed main.c
var main_c string.h files are embedded into the generated header, .c files into the generated implementation. The embed variable declarations are not emitted as C variables - they serve as markers only.
For ad-hoc C interop, the so/c package provides two compiler intrinsics that emit their string argument as raw C code. The argument must be a string literal.
c.Val[T](expr) emits a typed C expression. Use it to access C constants, macros, or call C functions inline:
nan := c.Val[float64]("NAN")
x := c.Val[float64]("sqrt(49)")c.Raw(code) emits a raw block of C code as a statement:
var b int
c.Raw(`
int a = 7;
b = a * a;
`)Be careful when using c.Val and c.Raw. C code written as string literals bypasses the type system and is hard to maintain, so it's usually better to use so:extern and so:embed instead.
The so/c package also provides low-level interop helpers for pointers, strings, and type information.
Functions:
AlignofandSizeofreturn the alignment and size of type T.Allocaallocates an array on the stack.Assertpanics with a message if a condition is false.Assumetells the C compiler that a condition is always true.Bytes,SliceandStringwrap C pointers to So types.CStringconverts a So string to a null-terminated C string.PtrAdd,PtrAsandPtrAtmanipulate pointers.Zeroreturns the zero value of type T.
Types:
CharandConstCharrepresent a Cchartype.Int,UInt,Long,ULong, etc. represent numeric C types.Size,SSize,PtrdiffandIntptrrepresent C types whose width follows the target.