Connect to a C++ library¶
Use a package wrapper to call a C++ library. This guide covers accepted types, ownership, callbacks, and errors.
At a glance
- Map C++ types and functions to Dream declarations with
@cpp. - A package wrapper owns the C++ library and its build settings.
C++: @cpp¶
Declare the C++ API you need in Dream. The header stays ordinary C++, with no annotations and no
extern "C":
// native/include/kv.hpp
namespace kv {
struct Entry { std::string key; const std::string& name() const { return key; } };
class Store {
public:
explicit Store(const std::string& path);
void put(const std::string& key, const std::string& value);
std::optional<std::string> get(const std::string& key) const;
Entry* find(const std::string& key); // nullptr on miss
void compact(); // may throw
void on_change(std::function<void(const std::string&)> f);
template <class T> T get_as(const std::string& key) const;
int scaled(int factor = 10) const;
static Store* open_default();
};
std::string greet(const std::string& who);
}
import system.interop;
// src/kvstore.dream
module kvstore;
@cpp("kv.hpp", "kv::Store")
public class Store {
extern constructor(path: string);
extern fun put(key: string, value: string): void;
extern fun get(key: string): Option<string>;
extern fun find(key: string): Option<Entry>; // borrowed; keeps the Store alive
extern fun compact(): Result<Unit, string>; // Ok(Unit()), or Err(e.what())
extern fun on_change(f: fun(string): void): void; // capturing closures are fine
@cpp_name("get_as<int>")
extern fun get_int(key: string): int;
extern fun scaled(): int; // C++ fills in `factor`
@owned
static extern fun open_default(): Store;
}
@cpp("kv.hpp", "kv::Entry")
public class Entry {
@cpp_name("name")
extern fun key(): string;
}
@cpp("kv.hpp", "kv::greet")
extern fun greet(who: string): string;
@cpp("header")or@cpp("header", "ns::Name")goes on a class, a value struct, or a freeextern fun. The header resolves against the set's include path, and the second argument defaults to the Dream name.- Members are
extern constructor(...), instanceextern fun, andstatic extern fun. A@cppclass holds onlyexternmembers. Put Dream helpers in anextend Store { ... }block or a wrapping class. - Visibility.
@cppmembers and free functions are public, orinternalwhen marked so. The class's ownpubliccontrols whether other files can see it. @cpp_name("expr")renames a member. It also selects a template instantiation (get_as<int>).- Overloads are ordinary Dream overloads; C++ picks the matching one.
- Default arguments. Declare fewer parameters and C++ fills in the rest. Dream-side defaults and variadics are rejected.
@cppmembers cannot be generic,async, operators, indexers, or accessors. Bind a named C++ function instead.
Bridgeable types¶
| Dream | C++ |
|---|---|
numbers, bool, char, byte |
the matching arithmetic type |
string |
std::string, const std::string&, std::string_view, const char* |
Option<string> |
std::optional<std::string>, or a nullable const char* |
CPtr, Option<CPtr> |
any pointer |
a @cpp class, Option<Class> |
T, T&, T*, std::unique_ptr<T>, std::optional<T> |
a @cpp value struct |
the struct by value or const T&; ref for T& |
T[] of numbers |
std::span<const T> or const std::vector<T>& |
fun(...) |
std::function<...> |
ref x: T (scalars, structs) |
T& |
Result<T, string> result |
T, with a thrown exception becoming Err(e.what()) |
Anything else is a diagnostic on the declaration.
Ownership¶
The real C++ return type decides ownership, and the shim resolves it at compile time:
- By value,
std::unique_ptr<T>, orstd::optional<T>: owned. The object moves to the heap and is deleted once, when the Dream object is released. T*orT&: borrowed. The Dream object holds a strong reference to the object it came from, so anEntryfromstore.find(...)keeps theStorealive.@ownedon a member returningT*takes ownership instead (factories).
A NULL pointer returned where the declaration is not Option traps with the member's name.
Exceptions¶
Every shim call catches C++ exceptions. With a Result<T, string> return, an exception becomes
Err(e.what()). Otherwise the program traps with the message. A void C++ member that may throw
declares Result<Unit, string> and yields Ok(Unit()), matching the stdlib convention.
Callbacks¶
fun(...) parameters map to std::function. The shim retains the closure for as long as the
std::function lives and releases it when C++ destroys it, so capturing closures need no
NativeCallback. The same thread rule applies: calls must arrive on a Dream thread.
Structs¶
A @cpp value struct holds only scalar fields. The shim checks its layout with static_assert
on sizeof and every offsetof. A mismatch fails the C++ compile, reported as:
import system.interop;
@cpp("kv.hpp", "geom::Point")
public struct Point {
public x: double;
public y: double;
}
Checked by the C++ compiler¶
The shim uses #line directives, so a declaration that does not match the header fails the build
at the .dream line that declared it:
The generated shim.cpp and dream_bridge.hpp are written next to the build output
(target/<profile>/native-c/<set>/) for inspection. A shim covers every @cpp declaration of a
set the program uses.