Skip to content

Latest commit

 

History

History
122 lines (101 loc) · 5.8 KB

File metadata and controls

122 lines (101 loc) · 5.8 KB

Caelum C++ code generator

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.moduleName and codegencpp.packageName (required);
  • codegencpp.objectName and codegencpp.clang (optional);
  • codegencpp.compilerArg.0, .1, ... for lossless ordered compiler arguments (or newline-separated codegencpp.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.

Supported surface

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.