Working with C
Salam talks to C libraries directly, with no glue code and no wrapper generator. That puts the C standard library and every other library written in C within reach of a Salam program.
extern
Inside an extern: block, declare C functions with their parameter and result types, then call them like any Salam function. The declarations have no body: the code lives in the C library, and Salam only needs to know how to call it.
extern:
func sqrt(x: f64): f64
func abs(x: int): int
end
func main:
println sqrt(2.0)
println abs(-42)
endThe compiler trusts these declarations completely. If a declaration does not match the real C function, for example an int where C expects a double, nothing warns you and the program reads garbage, so copy each signature carefully from the library's header or manual page.
Matching C types
| C | Salam |
|---|---|
int, unsigned int | int (i32), uint (u32) |
long long, int64_t, uint8_t, ... | i64, i64, u8, ... |
size_t | usize |
ssize_t, intptr_t | size |
double, float | f64, f32 |
const char* holding text | str |
char* or unsigned char* holding bytes | u8*, or void* |
void*, T* | void*, T* |
| a function pointer | void*, or extern func (...) R |
A Salam str already ends with a zero byte, so it can be passed straight to any C function that wants a string.
printf and other variadic functions
A C function that takes a variable number of arguments, such as printf, is declared with ... after its fixed parameters. The C format rules apply: %d for an int, %f for an f64, %s for a str, and widths such as %-8s and %6.2f for columns:
extern:
func printf(format: str, ...): int
func strlen(s: str): usize
func atoi(s: str): int
func toupper(c: int): int
end
func main:
println "Receipt"
printf("%-8s %3d x %6.2f\n", "coffee", 2, 3.5)
printf("%-8s %3d x %6.2f\n", "bagel", 1, 2.25)
qty := atoi("12") + atoi("30")
println "items:", qty
word := "salam"
println "letters:", strlen(word)
println "first:", char_from_code(toupper(char_code(word)))
endOutput from printf and from println comes out in the order the program wrote it, because Salam empties its own output buffer before calling a C function that writes. The %-8s padding counts bytes, not letters, so text in Persian or any other non-Latin script lines up differently; for anything but plain ASCII, println or the fmt package is the better tool.
Pointers, buffers and the zero byte
Many C functions write into memory you give them. Allocate it with mem, pass the pointer, and free it when you are done; defer right after the allocation keeps that from being forgotten. Index a pointer like an array: buf[i] reads or writes one element.
import mem
extern:
func snprintf(buf: void*, size: usize, format: str, ...): int
func getenv(name: str): str
end
func main:
buf := mem.AllocateZeroed(32 as u64) as u8*
defer mem.Free(buf as void*)
n := snprintf(buf as void*, 32 as usize, "%s-%05d", "INV", 42)
label := buf as str
println n, label
bytes := [72, 105, 0, 33, 33]
repeat 5 in i:
buf[i] = bytes[i] as u8
end
text := buf as str
println "as str:", len(text), "bytes"
mut sum := 0
repeat 5 in i:
sum += buf[i] as int
end
println "through the pointer:", sum
value := getenv("SALAM_EXAMPLE_UNSET")
if (value as void*) == null:
println "SALAM_EXAMPLE_UNSET is not set"
end
endWatch the middle of that output. A str ends at its first zero byte, so the five bytes H i 0 ! ! turn into the two-byte string Hi, and nothing reports it: the rest is simply gone. Text from C is fine as a str; binary data such as images, compressed files or network packets is not. Keep binary data as a pointer and a length, and read it through the pointer, as the loop that adds up all five bytes does.
A C function that may return no string, such as getenv for a variable that is not set, returns a null pointer. Check it with (value as void*) == null before you use the value. A string returned by C belongs to the C library, so never free it yourself.
Linking a library
Functions from the C standard library need nothing extra. For any other library, tell the linker which one to use with link, written before the extern block. The kind is always written out:
link dynamic "sqlite3"links the shared library, like-lsqlite3in a C buildlink static "z"asks for the static librarylibz.a, which is copied into the programlink framework "Cocoa"links a macOS framework
A bare link "m" is an error that names the three kinds. Since a framework exists only on macOS, wrap it in a compile-time if, as in this example, which links the C math library:
link dynamic "m"
if SALAM_OS_MAC:
link framework "CoreFoundation"
end
extern:
func fmod(x: f64, y: f64): f64
func hypot(x: f64, y: f64): f64
func round(x: f64): f64
end
func main:
println "17 minutes in 5 minute slots leaves", fmod(17.0, 5.0)
println "diagonal of a 3 by 4 screen:", hypot(3.0, 4.0)
println "2.5 rounded:", round(2.5)
endexport: Salam functions for C
Some C functions call you back. qsort sorts an array of any type, and asks a function of yours to compare two elements. A Salam function that C has to call goes in an export: block: it is then a real C function under its plain name, and &name is its address, which you pass where C expects a function pointer:
import mem
extern:
func qsort(base: void*, count: usize, width: usize, compare: void*)
end
export:
func by_score(a: void*, b: void*): int:
x := (a as i32*)[0]
y := (b as i32*)[0]
if x < y:
ret -1
else x > y:
ret 1
end
ret 0
end
end
func main:
scores := [42, 7, 19, 88, 3]
n := len(scores)
p := mem.Allocate((n * 4) as u64) as i32*
defer mem.Free(p as void*)
repeat n in i:
p[i] = scores[i]
end
qsort(p as void*, n as usize, 4 as usize, &by_score)
mut line := ""
repeat n in i:
line += " " + p[i]
end
println "sorted:" + line
endThe comparison gets two void* pointers to the elements, casts them to i32* and reads them with [0]. The numbers have to sit in memory that C can reach, so the program copies them into a buffer from mem, lets qsort sort them in place, and reads the result back through the pointer.
An exported function is always an ordinary public C symbol, which brings a few rules:
- every function in the block needs a body; declarations without one belong in
extern - no variables, no generic functions, and no
...parameters - no
pub,inlineornoinline; onlydeprecated,pureandnoretare allowed - the name must be a single word, because it becomes the C name, and is best kept to Latin letters: the LLVM backend cannot emit a non-ASCII export name yet
Calling through a C function pointer
The other direction works too. (&name) as extern func (f64) f64 turns the address of a C function, or of an exported Salam one, into a typed value you can store, pass around and call. extern func (...) R is also a parameter type, so one Salam function can work with any C function of that shape:
extern:
func sqrt(x: f64): f64
func cbrt(x: f64): f64
end
export:
func halve(x: f64): f64:
ret x / 2.0
end
end
func show(name: str, f: extern func (f64) f64, x: f64):
println name + "(" + x + ") =", f(x)
end
func main:
show("sqrt", (&sqrt) as extern func (f64) f64, 64.0)
show("cbrt", (&cbrt) as extern func (f64) f64, 64.0)
show("halve", (&halve) as extern func (f64) f64, 64.0)
endA plain Salam function name used as a value is its address as an i64, which is what callback tables such as a web router take. For a void* slot, or for a cast to a typed C function, write &name.
Rules of thumb
- Copy every C signature exactly; the compiler cannot check it.
- Use
strfor text and a pointer plus a length for bytes. - Free what you allocate with
mem, and nothing that C hands back unless its documentation says so. - Check pointers that C may return as null before using them.
- Prefer the Salam standard library when it already covers the job: it does the C calls for you and works the same on every system.
Different systems sometimes need different C functions. The extern block can go inside a top-level if SALAM_OS_WINDOWS:, as shown in Conditional compilation.