C
Base uses each platform's C conventions for laying out data and calling functions. A Base struct has the layout of the same C struct, a Base pointer is a C pointer, and every Base function can be called from C. So calling C needs only a declaration, and C can call Base through a generated header. Reference: Calling C.
Declaring C functions#
import c
extern struct DivResult:
quot: i32
rem: i32
extern func div(numerator: i32, denominator: i32) -> DivResult
extern func frexp(value: f64, out exponent: i32) -> f64
extern func strtol(text: c.str, out end: c.str, base: i32) -> c.long
extern type File
extern func fopen(path: c.str, mode: c.str) -> File?
extern func fclose(file: File) -> i32
pub func main(arguments: str[]) -> i32:
let result = div(17, 5)
print(f"{result.quot} {result.rem}")
let (mantissa, exponent) = frexp(48.0)
print(f"{mantissa} * 2^{exponent}")
let (value, rest) = strtol("123abc", 10)
print(f"{i64(value)} then '{str(rest) else "?"}'")
if let file = fopen("/definitely/not/here", "r"):
_ = fclose(file)
else:
print(f"fopen failed, errno {c.errno()}")
return 03 2 0.75 * 2^6 123 then 'abc' fopen failed, errno 2
| Declaration | Binds |
|---|---|
extern func name(...) -> R | a C function |
extern func base_name as "c_name"(...) | a C function under another name |
extern blocking func ... | a function that may block the thread, such as sleep or read; informational in Base |
extern struct Name: | a C struct, by value; fields name: Type, in C's order |
extern union Name: | a C union |
extern type Name | an opaque handle, such as FILE*: a pointer whose contents Base never sees |
extern type Name = u32 | an opaque handle that is an integer |
extern var name: T | a C global variable of scalar or pointer type |
outparameters are C's pointer parameters used to return extra results. Declared without, they are not passed; they come back after the result as a tuple, asfrexpandstrtolshow.- A struct passed or returned by value works, following the platform's rules for aggregates, as
divshows.extern structmay bepackedoralign(N). - Function pointers are written as Base function types; a Base function, or a lambda without captures, can be passed where C expects a callback. C's
void* user_datapattern is how state reaches the callback. strcannot appear in anexterndeclaration; C text isc.str.
Null pointers#
A pointer, function or handle declared without ? must not be null. Where one crosses from C, in a result or a callback argument, the compiler inserts one comparison, and a null there traps with null_foreign. Declare it ? when C may pass NULL, as fopen returns above. luce-base bind makes every pointer ? unless the header says otherwise.
C's types#
C's types are in the c module. Those with the same size everywhere are the Base types under another name, and can be written either way; the ones whose size depends on the platform are separate types that must be converted explicitly.
| C | Base | Notes |
|---|---|---|
int, unsigned int | i32, u32 (or c.int, c.uint) | 32 bits everywhere |
short, long long, signed char | i16, i64, i8 | and their unsigned forms |
float, double | f32, f64 | |
_Bool | bool | |
size_t, uintptr_t | usize | |
ssize_t, ptrdiff_t, intptr_t | isize | |
char | c.char | separate: signed on most targets, unsigned on ARM Linux |
long, unsigned long | c.long, c.ulong | separate: 64 bits on macOS and Linux, 32 on Windows |
wchar_t | c.wchar | separate: 32 bits, 16 on Windows |
va_list | c.va_list | can only be passed on to C |
const char* | c.str | text ending in a zero byte |
T*, void* | T*?, void*? | or T* when it is never null |
c.long(x) converts an i64 to c.long, checking that it fits; i64(value) converts back. long double, _Complex and bit-fields have no Base equivalent.
c.errno() and c.set_errno(value) read and write errno, and c.stdin(), c.stdout() and c.stderr() are C's standard streams.
Text#
- A string literal converts to
c.strautomatically, because every literal ends in a zero byte. str(value)reads ac.strback: it finds the zero byte, checks UTF-8, and returns astr(fallibly).(c.str)textpasses astr's pointer without copying. It is only correct when the text is followed by a zero byte, as literals andstrings.formatresults are. For any otherstr,strings.copy(text)makes a terminated copy, to be freed withstrings.release.
Variadic functions#
extern func printf(format: c.str, ...) -> i32Arguments in the ... position are passed as C would pass them: an integer literal as an int, a float literal as a double, small integers and bool promoted to int, f32 promoted to f64, and pointers as they are. A str, span, struct or optional cannot be passed there.
C's printf writes through C's buffered stdout, while print writes directly, so a program that uses both can see lines out of order.
C files and libraries#
A package can include C sources and link C libraries, through def native blocks in its manifest:
def native "inputs" {
str[] sources = ["vendor/stb_image.c", "native/shims.c"]
str[] libraries = ["sqlite3", "m"]
str[] link_search = ["/opt/homebrew/lib", "$VULKAN_SDK/lib"]
str[] pkg_config = ["sdl2"]
}
def native "macos" {
str[] frameworks = ["Metal", "QuartzCore"]
}- The block's name says where it applies:
inputs(orall) everywhere, an operating system (macos,linux,windows) on that system, or an exact target such asx86_64-windows. All the blocks that apply are combined. sourcesare compiled with the C compiler on your machine and linked into the program. Windows builds use no C compiler, so there a package with C sources stops with an error: name them underdef native "macos"anddef native "linux", and write the Windows side in Base.librariesare linked by name (-l), searched for inlink_searchdirectories as well. A directory starting with$NAMEuses that environment variable, and is skipped when it is not set.pkg_confignames libraries to look up withpkg-config;frameworksare macOS frameworks.
There is no inline C in a .lucb file; put C in a .c file and list it.
Generating declarations: luce-base bind#
luce-base bind geom.h -o geom.lucbreads a C header and writes a Base module of extern declarations for it, reporting anything it could not translate. For this header:
#include <stddef.h>
#define GEOM_VERSION 3
typedef struct { float x, y; } GeomPoint;
typedef enum { GEOM_OK = 0, GEOM_EMPTY = 1 } GeomStatus;
GeomStatus geom_centroid(const GeomPoint *points, size_t count, GeomPoint *out);
const char *geom_name(void);it writes:
import c
pub extern struct GeomPoint:
pub x: f32
pub y: f32
pub enum GeomStatus as u32:
GEOM_OK = 0
GEOM_EMPTY = 1
pub extern func geom_centroid(points: const GeomPoint*?, count: usize, out_: GeomPoint*?) -> GeomStatus
pub extern func geom_name() -> c.str?
pub let GEOM_VERSION: i32 = 3- Structs, unions, enums, typedefs, functions, constant
#defines,static constvalues andexternvariables are translated.static inlinefunctions are called through generated C shims, written with--shims shims.c. - A header usually does not say which pointers are never null, so every pointer becomes optional. A recipe file states what the header cannot, one line each:
nonnull geom_centroid.points,nonnull geom_name.return,skip NAME, andmacro PROTOTYPE;for a function-like macro, which is then called through a shim. - A name Base reserves gets an underscore:
outbecameout_. -I DIRand-D NAME=VALUEwork as for a C compiler.--preprocessshows what the tool read.- Not translated: function-like macros without a recipe line,
_Generic,long double,_Complex,__int128, bit-fields, and Objective-C.
The generated file is ordinary source: check it in and review it.
C calling Base#
export gives a function a C symbol:
export func blend(left: Pixel, right: Pixel) -> Pixel:
...
struct Cursor:
pub var offset: usize
export static func advance(count: usize) -> !:
...luce-base build file.lucb --lib -o libname writes a static library, libname.a, and a C header, libname.h, declaring everything exported.
- Only
exportfunctions get C symbols. Other functions,pubor not, have internal names that cannot collide with C's. - An exported method is named
Type_method, withselfas its first parameter. Asymbol_prefixin the manifest is put in front of every exported name. - What can be exported is what C can express: numbers,
bool, pointers,ctypes, structs and unions of those, integer-backed enums, and function pointers. A span parameter becomes two C parameters, a pointer and a length (data,data_len). A function that can fail becomes one returning anintstatus (0 for success, otherwise the error code) and writing its result through a last pointer parameter,out. - Not exportable:
str, spans in results or fields, optionals other than pointers, interfaces,fmt, and generic functions. Passconst u8*and a length instead ofstr.
The header spells structs as C structs, with packed and aligned attributes where declared, usize as size_t, function types as typedefs, integer-backed enums as an integer typedef plus constants, and each pub error code of the package as a #define.
Handles#
A handle is an opaque type with a function that releases it:
pub handle File:
destroy close
pub func open(path: str) -> File!:
...
pub func close(file: File):
...Inside Base a handle is like an extern type: a pointer that is never null (File? may be). The destroy function matters to code that imports the module from Luce, which treats a handle as an object whose close is called automatically when it is no longer used; see Luce.