Skip to content

C ABI Interop

Accepted

Complete for V1 scalar source semantics; unsupported-signature diagnostics remain implementation verification work.

Catalyst's first external ABI target is C. V1 keeps the interop surface small and deterministic for scalar functions. It is not a complete foreign-function interface.

Declaration Attributes

C ABI interop uses declaration attributes:

@export(.c)
fn scale(sample: f32, amount: f32) f32 {
  return sample * amount
}

@extern(.c)
@link_name("fabsf")
const fabs: *const @callconv(.c) fn(x: f32) f32

The .c arguments are enum-like cases completed by expected-type shorthand, not strings and not local identifiers. @callconv(.c) expects CallConv.c; @export(.c) and @extern(.c) expect InteropAbi.c.

Builtin or compiler-intrinsic expression syntax is deferred. @ is committed here as attribute syntax; builtin syntax should not casually reuse it unless that tradeoff is explicitly reopened.

V1 Meaning

V1 separates call ABI from symbol linkage:

  • @callconv(.c) sets the call ABI on a function declaration or function type expression.
  • @export(.c) exports a Catalyst free function definition as a C symbol and implies @callconv(.c).
  • @extern(.c) binds an external C symbol as a const value.
  • @link_name("...") overrides the external symbol name for @export(.c) or @extern(.c).

For V1, @export(.c) is valid only on free function definitions with bodies:

@export(.c)
fn scale(sample: f32, amount: f32) f32 {
  return sample * amount
}

It exports the definition as an external C ABI symbol, uses the C calling convention, defaults the symbol name to the Catalyst function name, and requires a C-ABI-safe parameter and return signature.

@callconv(.c) alone does not export or import anything:

@callconv(.c)
fn local_c_abi_helper(x: i32) i32 {
  return x
}

This function has C call ABI but no external linkage.

C Imports

C function imports are source-level external symbol bindings. In Catalyst V1, they are represented as const values with function pointer type:

@extern(.c)
@link_name("fabsf")
const fabs: *const @callconv(.c) fn(x: f32) f32

@extern(.c) is valid only on const declarations in V1. For C function imports, the const type must be an explicit function pointer whose pointee function type has @callconv(.c). The pointee function signature must use V1 C-ABI-safe types.

This is invalid because @extern(.c) does not mutate the declared type:

@extern(.c)
const fabs: *const fn(x: f32) f32 // invalid

Imported function pointer consts are called with normal function pointer call syntax:

var y = fabs(-1.0)

Bodyless C-like function declarations are deferred because they would add a special declaration form:

@extern(.c)
fn fabsf(x: f32) f32 // deferred

Symbol Names

The default exported C symbol name is the Catalyst function name:

@export(.c)
fn scale_f32(sample: f32, amount: f32) f32 {
  return sample * amount
}

exports:

scale_f32

Rules:

  • exported names are unmangled
  • default export names and default extern names must be portable ASCII C identifiers: [_A-Za-z][_A-Za-z0-9]*
  • @export(.c) defaults to the function name
  • @extern(.c) defaults to the const binding name
  • @link_name("...") sets the exact external symbol name

@link_name requires @export(.c) or @extern(.c) in V1. It is invalid with @callconv alone.

Duplicate C link names are checked within the visible linkage set:

  • duplicate exported link names are hard errors
  • duplicate visible imports with the same link name and same type are allowed but lintable as interop/duplicate-import
  • duplicate visible imports with the same link name and incompatible types are hard errors

Visibility

@export(.c) controls C ABI visibility. It is separate from Catalyst module visibility.

Plain functions are internal/private to generated C by default:

fn helper(x: f32) f32 {
  return x * x
}

@export(.c)
fn scale(sample: f32, amount: f32) f32 {
  return helper(sample) * amount
}

The C backend direction is:

static float helper(float x) { ... }
float scale(float sample, float amount) { ... }

Internal symbol naming is backend-defined but must be deterministic. Future pub module visibility must not imply C export by itself.

ABI-Safe Types

V1 C function signatures support only the V1 scalar core types:

  • void
  • bool
  • i32
  • f32

Catalyst error-return types are not C-ABI-safe in V1:

@export(.c)
fn load(path: Path) Module!LoadError // invalid for v1 C export

This rejection applies to every V1 C ABI signature validation path, including @export(.c) declarations, @extern(.c) function pointer types, and function declarations or function type expressions marked with @callconv(.c). Inference may resolve first, but any resolved T!E in a C ABI signature is a hard ABI-safety error.

The C representation for T!E should be designed deliberately later, for example through explicit result structs or out-parameters. Broader ABI-safe type expansion is deferred to CEP-0007: C ABI Compound Types and Error Returns. Each type must have a deliberate ABI mapping before becoming export-safe.

ABI Inference

@export(.c) functions follow normal Catalyst function inference rules. The compiler should not introduce special cases that forbid inference only because a function is exported.

However, exported ABI is a public contract. First-class linting should warn when an exported C ABI signature relies on inferred parameter, return, or error facts.

Example lint:

exported C ABI function has inferred return type

Phase Contract

Attributes are parsed as source-preserved metadata. Sema resolves and evaluates the attribute provider, then validates the resulting ABI/export facts and ABI-safe signatures.

Resolved ABI, import, export, and link-name facts must be carried into SIR and IR before backend emission. The C backend consumes backend-facing facts such as:

  • calling convention
  • linkage status
  • external symbol name
  • ABI-safe lowered types

The C backend must not inspect AST attributes or rerun semantic ABI checks.

Future Decisions

Deferred: