cgo-gen is a Rust CLI that parses a conservative subset of C/C++ headers and generates:
- C ABI wrapper headers and sources
- optional normalized IR dumps
- Go
cgofacade files beside the generated native wrapper
It is designed for controlled C/C++ header surfaces, not for arbitrary modern C++ codebases.
From a repository checkout, run check first against the smallest example, then
generate wrappers from the same config:
cargo run --bin cgo-gen -- check --config examples/01-c-library/config.yaml
cargo run --bin cgo-gen -- generate --config examples/01-c-library/config.yaml --dump-irThat flow:
- load a YAML config
- parse headers with
libclang - normalize declarations into IR
- generate wrapper files into
output.dir - optionally dump the generated
.ir.yamlfile
After installing cgo-gen, use the same flow with your own config:
cgo-gen check --config path/to/config.yaml
cgo-gen generate --config path/to/config.yaml --dump-irFor a guided progression, see the numbered examples.
- Rust toolchain
libclangavailable at runtime- a Clang-compatible compile environment for non-trivial headers
- Go toolchain only if you plan to build generated Go packages
cgo-gen uses libclang to preprocess, parse, and type-check C/C++ headers.
- if
libclangis installed in a non-standard location, setLIBCLANG_PATH
Typical install paths:
- Windows
winget install LLVM.LLVM- if needed, set
LIBCLANG_PATHto the LLVMbindirectory, for exampleD:\programs\LLVM\bin - for Mingw64,
pacman -S mingw64/mingw-w64-x86_64-clang
- macOS
- Homebrew:
brew install llvm - MacPorts:
port install clang - Homebrew LLVM installs
libclang.dylibunder$(brew --prefix llvm)/lib. If test binaries cannot loadlibclang.dylib, run tests with:DYLD_LIBRARY_PATH="$(brew --prefix llvm)/lib" cargo test
- If
xcode-select -ppoints at a stale or misspelled Xcode path, either fix it:or override it per command:sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer \ DYLD_LIBRARY_PATH="$(brew --prefix llvm)/lib" \ cargo test --test overload_collisions
- Homebrew:
- Debian/Ubuntu
apt install libclang-dev- install
clangas well if you need the full Clang CLI locally
- Arch
pacman -S clang
- Fedora
dnf install clang-devel
- OpenBSD
pkg_add llvm- if needed, set
LIBCLANG_PATH=/usr/local/lib
If your package manager does not provide a recent enough Clang/libclang, build from source. For this project you only need the Clang pieces, not the full LLVM optional stack.
Run from the repository:
cargo run --bin cgo-gen -- --helpOr install locally:
cargo install --path .
cgo-gen --helpcgo-gen currently exposes three subcommands:
generate --config <path> [--dump-ir] [--go-module <module-path>]ir --config <path> [--output <path>] [--format yaml|json]check --config <path>
Typical flow:
cgo-gen check --config path/to/config.yaml
cgo-gen generate --config path/to/config.yaml --dump-irUse ir when you want to inspect the normalized model without writing wrapper files:
cgo-gen ir --config path/to/config.yaml --format yamlThe supported config surface is intentionally small:
version: 1
input:
dirs:
- path/to/include
clang_args:
- -std=c++17
owner:
- WidgetFactory::Create
ldflags:
- -Lpath/to/lib
- -lfoo
output:
dir: genUse input.headers instead of input.dirs when you want to wrap an exact list of entry headers:
version: 1
input:
headers:
- path/to/include/widget.hpp
- path/to/include/service.hpp
clang_args:
- -Ipath/to/include
output:
dir: genKey behaviors:
- relative paths are resolved from the config file location
- unknown keys are rejected at load time
- each
input.dirsentry is scanned directly; nested directories are ignored unless listed explicitly - every
input.dirsentry is treated as an owned header root and added to libclang include flags input.headersis an exact file list and cannot be combined withinput.dirs- headers included by listed files are parsed as dependencies, but wrappers are generated only for files listed in
input.headers - generated
.go,.h,.cpp, and optional.ir.yamlfiles are written together underoutput.dir - object-like integer, floating-point, and ordinary string literal macros are emitted as Go constants
output.go_versioncontrols generatedgo.modfiles and defaults to1.26- when
--go-module <module-path>is set,generatealso writesgo.modandbuild_flags.go
For each supported entry header, generate can emit:
<name>_wrapper.h<name>_wrapper.cpp<name>_wrapper.go<name>_wrapper.ir.yamlwhen--dump-iris enabled
When --go-module is set, it also writes:
go.modbuild_flags.go
The generated files are intentionally co-located so a downstream cgo package can compile them as one package-local unit.
IR source_headers entries are written relative to the generated .ir.yaml file so checked-in examples are independent of the clone location.
Use generate --go-module <module-path> when you want output.dir to behave like a standalone Go module:
cgo-gen generate --config path/to/config.yaml --go-module example.com/acme/fooWhen enabled, generate also writes:
go.modwithmodule <module-path>andgo <output.go_version>; the default is1.26build_flags.go
Current behavior:
build_flags.goalways emits#cgo CFLAGS: -I${SRCDIR}#cgo CXXFLAGSare exported from the safe subset of rawinput.clang_args- exported
CXXFLAGSallow only-I,-D, and-std=... - when
input.ldflagsis set,build_flags.goalso emits#cgo LDFLAGS
Use this mode when the generated directory should carry Go module metadata.
Native headers, sources, and libraries still need to be made available to the
Go build through include paths, copied headers, or input.ldflags.
You do not need many knobs to get started. These are the supported ones:
input.dirs: direct input directories used for owned header discovery and translation-unit discovery; list nested directories explicitlyinput.headers: exact entry header list, resolved from the config file location; mutually exclusive withinput.dirsinput.clang_args: extra libclang flags such as-I...,-isystem...,-D...,-std=...input.owner: qualified callable names whose pointer returns should be emitted as owned Go wrappersinput.ldflags: linker flags forwarded into generatedbuild_flags.gooutput.dir: output directoryoutput.header,output.source,output.ir: optional explicit filenames for single-header generation
Important caveats:
- if you use multi-header generation, leave
output.header,output.source, andoutput.irat their defaults - generated C symbol naming is fixed in source and is not configurable via YAML
input.dirs,input.headers,input.clang_args, andinput.ldflagsresolve relative paths from the config file directory- use
input.owneronly when a pointer return actually transfers ownership, for example a factory method that returnsnew-allocated objects input.ownermatches by qualified callable name such asWidgetFactory::Create; if the same name is overloaded, every matching overload is treated as owned- env expansion supports
$VAR,$(VAR), and${VAR}only
For large libraries, either put the small header surface you want to wrap in one or more adapter directories and list them in input.dirs, or use input.headers to name the exact entry headers.
- free functions
- non-template classes
- constructors and destructors
- public methods with deterministic overload disambiguation
- public struct field accessors for supported field types
- primitive scalars and common fixed-width aliases
const char*,char*,std::string, andstd::string_view- fixed-size primitive and model arrays
- primitive pointer/reference write-back in Go
- recognized model by-value parameters and returns through handle-backed wrappers
- named callback typedefs used by supported APIs
struct timeval*andstruct timeval&- member and free operators with supported signatures, generated as named
Oper...wrappers - handle-backed Go wrappers for supported object paths
- allocator operators such as
operator newandoperator deleteare not exposed as wrapper targets - raw inline function pointer parameters such as
void (*cb)(int) - templates and STL-heavy APIs
- anonymous classes
- exception translation
- advanced inheritance modeling
- unknown or raw-unsafe by-value object parameters or returns outside recognized model paths
Unsupported declarations may be skipped instead of aborting the whole run. When that happens, the reason is recorded in support.skipped_declarations in the normalized IR.