-
Notifications
You must be signed in to change notification settings - Fork 3
C c++
Kairo provides zero-overhead, bidirectional interoperability with C and C++. There is no serialization layer, no binding generator, and no runtime bridge Kairo emits ABI-compatible object code and consumes C/C++ headers directly.
This page covers the full interop surface: calling C/C++ from Kairo, exposing Kairo to C/C++, inline C++ blocks, pointer and reference passing, templates and concepts across the boundary, exception interop, and the underlying ABI contract.
The table below summarizes which C and C++ features Kairo can consume and expose. Rows marked bidirectional work in both directions.
| Feature | Direction | Notes |
|---|---|---|
| Functions | Bidirectional | Includes variadic functions |
| Structs | Bidirectional | Layout-compatible; see Structs |
| Unions | Bidirectional | See Unions |
| Enums | Bidirectional | See Enums |
| Classes | Bidirectional | Vtable-compatible; see Classes |
| Templates | Bidirectional | Instantiation across the boundary; see below |
| Concepts | Bidirectional | Kairo's impl constraints map to C++20 concepts |
| Namespaces | Bidirectional | |
| Pointers & References | Bidirectional | Requires unsafe on the Kairo side; see below
|
| Operator Overloading | Bidirectional | See Operators |
| Lambdas | Bidirectional | |
| Exceptions | C++ -> Kairo | Kairo -> C++ is on the roadmap |
| Macros | C++ -> Kairo | Preprocessor macros are expanded before Kairo sees them |
| Preprocessor Directives | C++ -> Kairo | |
| Inline Assembly | Kairo -> C++ | Via inline "c++" blocks |
| Coroutines | Bidirectional | |
Named Modules (import std;) |
Not yet | See Modules note |
Import a C or C++ header with the ffi directive. The compiler parses the header, extracts declarations, and
makes them available as native Kairo symbols no wrapper code required.
// main.k
ffi "c++" import "my_code.hh";
fn main() {
var obj = MyClass("Kairo")
std::println(f"name = {obj.get_name()}")
my_function(42)
}
Given this C++ header:
// my_code.hh
#include <string>
#include <iostream>
class MyClass {
public:
MyClass(std::string name) : name(name) {}
std::string get_name() const { return name; }
private:
std::string name;
};
void my_function(int x) {
std::cout << "Hello from C++! x = " << x << std::endl;
}Build and run:
kairo main.k
./mainname = Kairo
Hello from C++! x = 42
[!NOTE]
ffi "c++"invokes Clang's frontend internally to parse the header. All exported declarations functions, classes, enums, templates become available in Kairo's scope with their original names and signatures. No code generation or binding step is visible to the user.
Going the other direction requires the kcc driver, a libclang-based compiler wrapper that makes
#include "file.k" work transparently in C++ translation units.
// my_code.k
fn my_kairo_function(x: i32) {
std::println(f"Hello from Kairo! x = {x}")
}
class MyKairoClass {
pub var name: string
fn MyKairoClass(self, name: string) {
self.name = name
}
fn get_name(self) -> string {
return self.name
}
}
// main.cpp
#include "my_code.k"
#include <iostream>
int main() {
MyKairoClass obj("C++");
std::cout << "name = " << obj.get_name() << std::endl;
my_kairo_function(42);
return 0;
}kcc main.cpp -o main
./mainname = C++
Hello from Kairo! x = 42
kcc is a Clang driver with a single addition: a preprocessor hook that intercepts #include directives. When
the included file has a .k extension, kcc:
- Invokes the Kairo compiler in-process as a library to produce a C++-compatible header containing forward declarations and wrapper signatures.
- Compiles the
.kfile into an object file. - Links the Kairo object into the final binary at the end of the pipeline.
Auto-linking can be disabled with -fno-kairo-link if you need manual control over the link step.
If you prefer a standard C++ build process, compile the Kairo source to a static library and a generated header, then link normally:
kairo my_code.k -c -o my_code -xc++ -header my_code.hh
g++ main.cpp my_code.o -o mainTip
kcc is the simplest path for mixed codebases. The manual workflow is better when Kairo
is a dependency consumed by an existing CMake/Meson/Bazel project that manages its own link step.
ffi controls linkage and name mangling. It can be applied to individual declarations or to blocks.
// C++ linkage name mangling, overloading, classes, templates all permitted
ffi "c++" {
fn compute(x: i32) -> i32 {
return x + 1;
}
}
// C linkage no name mangling, same restrictions as extern "C" in C++
ffi "c" fn add(x: i32, y: i32) -> i32 {
return x + y;
}
ffi "c" follows the same rules as extern "C" in C++: no classes, no overloading, no templates.
ffi "c++" follows the same rules as extern "C++": full C++ feature set, Itanium or MSVC mangling depending
on the target.
For small amounts of C++ that don't warrant a separate header, use inline "c++" blocks directly in Kairo
source.
import libcxx::iostream::{cout, endl};
fn main() {
inline "c++" {
// std:: here refers to Kairo's standard library, not libstdc++
std::println("Hello from inline C++!");
// C++ standard library types use the libcxx:: prefix
cout << "Hello from libcxx!" << endl;
}
}
Warning
Inside inline "c++" blocks, std:: resolves to the Kairo standard library
(the block emits inside namespace kairo). Access the C++ standard library through the libcxx module,
importing the specific header you need (e.g., import libcxx::vector;). See Modules for
more on imports.
Kairo's pointer model distinguishes safe pointers (*T, non-nullable, bounds-tracked) from raw
pointers (unsafe *T, no tracking). Passing any pointer or reference across the FFI boundary requires explicit
unsafe context because the compiler cannot enforce safety guarantees on the C/C++ side.
ffi "c++" import "my_code.hh";
fn main() {
var x = 41
// compile error: cannot pass reference to C function without unsafe block
// add_one(&x)
unsafe {
add_one(unsafe &x) // strips bounds checking; caller owns the memory contract
}
// for calling c++ with pointers and back, you must use unsafe blocks,
// short hand syntax is not allowed `unsafe add_one(unsafe &x)` is a compile error
std::println(f"x = {x}") // x = 42
}
unsafe & creates a raw pointer from a safe binding. The compiler relinquishes tracking for that pointer the
caller is responsible for lifetime and aliasing correctness.
If the value will be passed to C/C++ repeatedly, allocate it as a raw pointer upfront:
ffi "c++" import "my_code.hh";
fn main() {
var x: unsafe *i32 = std::create<i32>(41)
add_one(x) // already unsafe no block needed
std::println(f"x = {*x}") // x = 42
}
Warning
std::create<T> uses Kairo's global allocator, which is compatible with
C++'s new/delete by default. If you supply a custom allocator, C++ code must not free the resulting
pointer with delete doing so is undefined behavior. See AMT for allocator details.
Warning
unsafe *T pointers can be null. Dereferencing a null unsafe *T is undefined
behavior the compiler will not insert a null check.
Kairo generics and C++ templates are interchangeable across the boundary. A C++ concept can constrain a Kairo generic parameter, and a Kairo generic type can satisfy a C++ concept.
// my_code.hh
#include <concepts>
template<typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::same_as<T>;
};// MyInt.k
ffi "c++" import "my_code.hh";
class <T> MyInt {
pub var value: T
fn MyInt(self, value: T) {
self.value = value
}
fn op + (self, other: MyInt) -> MyInt {
return MyInt(self.value + other.value)
}
}
fn <T impl Addable> add(a: T, b: T) -> T {
return a + b
}
// main.cpp
#include <iostream>
#include "my_code.hh"
#include "MyInt.k"
int main() {
MyInt<int> a(5), b(10);
MyInt<int> c = add(a, b);
std::cout << "c.value = " << c.value << std::endl; // 15
int x = add(3, 4);
std::cout << "x = " << x << std::endl; // 7
}T impl Addable in Kairo maps directly to Addable T in the generated C++ the constraint is preserved across
the boundary, not erased.
Kairo can catch C++ exceptions using its standard try/catch syntax.
ffi "c++" import "my_code.hh";
fn main() {
try {
might_throw(true);
} catch e: std::exception {
std::println(f"Caught: {e.what()}")
}
}
Note
Unlike Kairo's panic system, the compiler cannot statically
determine every exception type a C++ function might throw. A catch block that doesn't handle a thrown type
will propagate the exception up the stack. If nothing catches it, the runtime calls std::terminate.
Throwing Kairo exceptions into C++ is not yet supported. Use error codes or return types
(e.g., Panickable<T>) for error signaling in the Kairo -> C++ direction.
Kairo emits object code conforming to the platform's native C++ ABI:
- Unix-like systems: Itanium C++ ABI
- Windows: Microsoft C++ ABI
This means Kairo .o/.obj files can be linked with object files from any ABI-compliant C++ compiler (GCC,
Clang, MSVC) without shims or translation layers. Name mangling, vtable layout, RTTI, and exception unwinding
tables all follow the platform convention.
ffi "c++" declarations use C++ mangling. ffi "c" declarations use C mangling (no decoration). This matches
the behavior of extern "C++" and extern "C" in C++.
C++20 named modules (import std;, import my_module;) are not currently supported. The interop layer
relies on header-based inclusion via Clang's preprocessor, and module interface deserialization (consuming
pre-compiled BMIs) is a future roadmap item.
Exporting Kairo code as a C++ module interface unit (.cppm) is also planned but not yet implemented.
For now: use header-based interop (ffi "c++" + kcc) for all cross-language boundaries.
This wiki mirrors the language reference at kairolang.org/docs. To edit a page, edit the source at kairo-web/src/content/docs/language changes sync automatically.
Start here: Primitives
1. Fundamentals
2. Functions & Control Flow
3. Types
4. Modules & Metaprogramming
5. Memory & Safety
6. Interop & Concurrency