codegen-cpp reads a C++ header with Clang's semantic JSON AST and emits:
- a deterministic
extern "C"shim header; - a C++ shim implementation that catches exceptions at the ABI boundary; and
- a Kotlin/JDK FFM binding that resolves symbols from one explicitly loaded library.
The generator is available both as a library API and as a Ktgen processor. A minimal library invocation is:
val generated = CppCodegen.generate(
header = Path.of("include/example.hpp"),
outputDirectory = Path.of("build/generated/cpp-binding"),
config = CppCodegenConfig(
moduleName = "example",
kotlinPackage = "com.example.binding",
),
)Ktgen discovers CppCodegenProcessor through ServiceLoader. It accepts exactly
one root header and uses these system properties:
codegencpp.moduleNameandcodegencpp.packageName(required);codegencpp.objectNameandcodegencpp.clang(optional);codegencpp.compilerArg.0,.1, ... for lossless ordered compiler arguments (or newline-separatedcodegencpp.compilerArgs).
The processor returns the generated shim header, shim source, Kotlin source, and diagnostics file to Ktgen.
Compile generated.source together with the header-only library (or its normal
implementation sources) as a shared library. The buildsrc.convention.native-library
plugin provides the matching host compiler task for projects in this repository.
When both conventions are applied, codegenCpp owns the public ABI inputs:
its includeDirs and apiDefines are forwarded to both Clang and the native
compiler, and the generated <moduleName>_shim.cpp is automatically registered
as a native source with an implicit compileNativeLibrary -> ktgen dependency.
The convention also recursively registers every regular file below those include
roots as a native compilation input. This intentionally covers conventional
headers as well as extensionless files and project-specific include fragments;
an edited transitive public include therefore reruns the native compiler even
when Ktgen regenerates identical shim text. Include directories themselves
remain path-only compiler settings for native-only projects, which can instead
choose the exact files to track through nativeLibrary.dependencyHeaders.
nativeLibrary.sources remains available for ordinary implementation sources;
headers plus implementationDefines generate the single include-based
implementation TU used by STB-style libraries.
Inside this repository the Gradle integration is:
plugins {
id("buildsrc.convention.codegen-cpp")
id("buildsrc.convention.native-library")
}
codegenCpp {
moduleName.set("example")
packageName.set("com.example.binding")
// Shared with the AST parser and the native compiler. Put public feature
// switches and all public-header include roots here exactly once.
includeDirs.from(layout.projectDirectory.dir("include"))
apiDefines.put("EXAMPLE_ENABLE_FEATURE", "1")
}
dependencies {
ktgenInput(files("include/example.hpp"))
}
nativeLibrary {
libraryName.set("example")
// No generated shim source or dependsOn("ktgen") is required here.
// This remains additive: implementation sources may be appended normally.
sources.from(layout.projectDirectory.file("src/main/cpp/example.cpp"))
// For a header-only library instead of example.cpp:
// headers.from(layout.projectDirectory.file("include/example.hpp"))
// implementationDefines.put("EXAMPLE_IMPLEMENTATION", "")
}nativeLibrary.apiDefines and nativeLibrary.includeDirs still exist for
native-only projects. Do not use them for declarations generated by
codegenCpp: configuring those public ABI inputs in codegenCpp prevents the
parser and compiled shim from drifting. implementationDefines are deliberately
native-only because they select implementation bodies, not the generated API.
The generated Kotlin object owns a shared FFM arena. load(Path) loads one
explicit native library once; calling load again, using the object after
close, or loading a second library is rejected. close is idempotent. The
opaque class wrappers generated for owned C++ objects likewise reject
use-after-close and make destruction idempotent. The generator does not infer
library discovery or package a platform binary; the native-library convention
compiles and packages the host platform's shared library, so applications must
choose the matching artifact and pass its path to load.
The current deliberately small ABI-safe surface is namespace free functions and overloads, scoped enums, opaque classes, public constructors/destructors, instance and static methods, and primitive/enum/pointer parameters and results. References, arrays, variadics, templates, C++ record values, STL types, inheritance, callbacks, and inferred ownership are not lowered. Unsupported declarations are written to the diagnostics output rather than assigned a guessed ABI.
Generated operation symbols are based on the qualified signature and a stable hash. Every operation returns a status; non-void values use an out parameter, and C++ exceptions are exposed through a thread-local last-error string.
Only declarations whose Clang source location belongs to the root header are emitted; declarations from system and transitive includes are intentionally ignored. Generated owned class wrappers reject method use after close and make close idempotent.
Clang and the compiler used for the shim must target a compatible C++ ABI. The
current model is intended for 64-bit host builds and does not promise cross-target
class or primitive ABI compatibility. The native-library task currently emits
one host-platform binary per build (Windows .dll, Linux .so, or macOS
.dylib) and rejects 32-bit hosts; cross-compilation and multi-platform
publication are outside this convention's scope.