Luce Base
LuceSourceLuciaOS

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 0
output
3 2
0.75 * 2^6
123 then 'abc'
fopen failed, errno 2
DeclarationBinds
extern func name(...) -> Ra 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 Namean opaque handle, such as FILE*: a pointer whose contents Base never sees
extern type Name = u32an opaque handle that is an integer
extern var name: Ta C global variable of scalar or pointer type
  • out parameters are C's pointer parameters used to return extra results. Declared with out, they are not passed; they come back after the result as a tuple, as frexp and strtol show.
  • A struct passed or returned by value works, following the platform's rules for aggregates, as div shows. extern struct may be packed or align(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_data pattern is how state reaches the callback.
  • str cannot appear in an extern declaration; C text is c.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.

CBaseNotes
int, unsigned inti32, u32 (or c.int, c.uint)32 bits everywhere
short, long long, signed chari16, i64, i8and their unsigned forms
float, doublef32, f64
_Boolbool
size_t, uintptr_tusize
ssize_t, ptrdiff_t, intptr_tisize
charc.charseparate: signed on most targets, unsigned on ARM Linux
long, unsigned longc.long, c.ulongseparate: 64 bits on macOS and Linux, 32 on Windows
wchar_tc.wcharseparate: 32 bits, 16 on Windows
va_listc.va_listcan only be passed on to C
const char*c.strtext 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.str automatically, because every literal ends in a zero byte.
  • str(value) reads a c.str back: it finds the zero byte, checks UTF-8, and returns a str (fallibly).
  • (c.str)text passes a str's pointer without copying. It is only correct when the text is followed by a zero byte, as literals and strings.format results are. For any other str, strings.copy(text) makes a terminated copy, to be freed with strings.release.

Variadic functions#

extern func printf(format: c.str, ...) -> i32

Arguments 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 (or all) everywhere, an operating system (macos, linux, windows) on that system, or an exact target such as x86_64-windows. All the blocks that apply are combined.
  • sources are 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 under def native "macos" and def native "linux", and write the Windows side in Base.
  • libraries are linked by name (-l), searched for in link_search directories as well. A directory starting with $NAME uses that environment variable, and is skipped when it is not set.
  • pkg_config names libraries to look up with pkg-config; frameworks are 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.lucb

reads 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 const values and extern variables are translated. static inline functions 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, and macro PROTOTYPE; for a function-like macro, which is then called through a shim.
  • A name Base reserves gets an underscore: out became out_.
  • -I DIR and -D NAME=VALUE work as for a C compiler. --preprocess shows 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 export functions get C symbols. Other functions, pub or not, have internal names that cannot collide with C's.
  • An exported method is named Type_method, with self as its first parameter. A symbol_prefix in the manifest is put in front of every exported name.
  • What can be exported is what C can express: numbers, bool, pointers, c types, 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 an int status (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. Pass const u8* and a length instead of str.

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.