Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions .github/workflows/build-macos-reusable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,21 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4

- name: Install LLVM Clang
- name: Install and configure LLVM Clang (GitHub-hosted)
if: ${{ !startsWith(inputs.runner-type, 'self-hosted') }}
run: |
brew install llvm
echo "$(brew --prefix llvm)/bin" >> $GITHUB_PATH
LLVM_BIN="$(brew --prefix llvm)/bin"
echo "LLVM_BIN=$LLVM_BIN" >> $GITHUB_ENV
# Use libc++ so <print> (C++23) and <expected> are available
echo "STDLIB_FLAG=-stdlib=libc++" >> $GITHUB_ENV

- name: Configure LLVM Clang (self-hosted macOS)
if: ${{ startsWith(inputs.runner-type, 'self-hosted') }}
run: |
# Self-hosted macOS runner should already have a C++23-capable clang.
# Ensure libc++ is used for <print> support.
echo "STDLIB_FLAG=-stdlib=libc++" >> $GITHUB_ENV

- name: Setup CMake 3.30+ and Ninja
uses: lukka/get-cmake@latest
Expand All @@ -57,12 +67,19 @@ jobs:
- name: Configure CMake (GitHub runner - universal binary)
if: ${{ !startsWith(inputs.runner-type, 'self-hosted') }}
run: |
cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{inputs.build-type}} -GNinja -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64"
cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{inputs.build-type}} -GNinja \
-DCMAKE_C_COMPILER=${LLVM_BIN}/clang \
-DCMAKE_CXX_COMPILER=${LLVM_BIN}/clang++ \
-DCMAKE_CXX_FLAGS="${STDLIB_FLAG}" \
-DCMAKE_EXE_LINKER_FLAGS="${STDLIB_FLAG}" \
-DCMAKE_OSX_ARCHITECTURES="arm64;x86_64"

- name: Configure CMake (self-hosted - native architecture)
if: ${{ startsWith(inputs.runner-type, 'self-hosted') }}
run: |
cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{inputs.build-type}} -GNinja
cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{inputs.build-type}} -GNinja \
-DCMAKE_CXX_FLAGS="${STDLIB_FLAG}" \
-DCMAKE_EXE_LINKER_FLAGS="${STDLIB_FLAG}"

- name: Build kwxgen
run: |
Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/build-unix-reusable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ jobs:

- run: mkdir ${{github.workspace}}/build

- name: Install libc++ and set stdlib flag (self-hosted)
if: ${{ inputs.runner-type == 'self-hosted' }}
run: |
# Self-hosted runner has clang-22 pre-installed. Ensure libc++ is
# available so <print> (C++23) can be found.
sudo apt-get install -y libc++-22-dev libc++abi-22-dev
echo "STDLIB_FLAG=-stdlib=libc++" >> $GITHUB_ENV

- name: Install LLVM Clang 21 (GitHub-hosted)
if: ${{ inputs.runner-type != 'self-hosted' }}
run: |
Expand Down
127 changes: 50 additions & 77 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,34 @@ The sections below detail the exact steps for each language.

---

### Fortran (kwxFortran)

**Prerequisites:** CMake 3.30+ and a C++23 compiler

**Step 1 — Build kwxgen and generate** (from CMakeLists.txt or a script):

```sh
cmake -S extern/kwxFFI/tools/kwxgen -B build/kwxgen -G Ninja
cmake --build build/kwxgen

build/kwxgen/kwxgen generate \
--headers extern/kwxFFI/include \
--defs extern/kwxFFI/src/kwx_defs.cpp \
--lang fortran \
--out src/gen/
```

**Generated output** (`src/gen/`): one `wx_button.f90`-style `module` per class using
`ISO_C_BINDING` `interface` blocks, plus `wx_events.f90`, `wx_keys.f90`,
`wx_constants.f90`. Compile and `use` normally:

```fortran
use wx_button
use wx_events
```

---

### Go (kwxGO)

**Prerequisites:** CMake 3.30+, a C++23 compiler.
Expand Down Expand Up @@ -114,102 +142,47 @@ Regenerate any time kwxFFI is updated; commit the generated files alongside hand

---

### Rust (kwxRust)

**Prerequisites:** CMake 3.30+ and a C++23 compiler

**Step 1 — Invoke kwxgen from `build.rs`:**

```rust
fn main() {
// Build kwxgen if not already built
let kwxgen = std::process::Command::new("cmake")
.args(["--build", "build/kwxgen"])
.status().expect("cmake build failed");

// Generate Rust bindings
std::process::Command::new("build/kwxgen/kwxgen")
.args([
"generate",
"--headers", "extern/kwxFFI/include",
"--defs", "extern/kwxFFI/src/kwx_defs.cpp",
"--lang", "rust",
"--out", "src/gen/",
])
.status().expect("kwxgen failed");

println!("cargo:rerun-if-changed=extern/kwxFFI/include/kwx_classes.h");
println!("cargo:rerun-if-changed=extern/kwxFFI/include/kwx_events.h");
}
```

**Generated output** (`src/gen/`): `sys.rs` with `extern "C"` declarations for all
functions; one `button.rs`-style file per class with a safe wrapper struct, `impl` block,
and `Drop` for classes that have a `Delete` method. Include from `lib.rs`:

```rust
mod gen;
pub use gen::button::Button;
```

---

### Perl (kwxPerl)
### TypeScript (kwxTypeScript)

**Prerequisites:** CMake 3.30+ and a C++23 compiler; Perl 5.32+ with `FFI::Platypus` 2.00+.
**Prerequisites:** CMake 3.30+ and a C++23 compiler; Deno 1.40+.

**Step 1 — Build kwxgen and generate** (from `Makefile.PL` or a setup script):
**Step 1 — Build kwxgen:**

```sh
cmake -S extern/kwxFFI/tools/kwxgen -B build/kwxgen -G Ninja
cmake --build build/kwxgen
```

**Step 2 — Generate bindings** (from a `Makefile` or your build script):

```sh
build/kwxgen/kwxgen generate \
--headers extern/kwxFFI/include \
--defs extern/kwxFFI/src/kwx_defs.cpp \
--lang perl \
--out lib/Wx/Gen/
--lang typescript \
--out gen/
```

**Generated output** (`lib/Wx/Gen/`): one `.pm` file per class using
`FFI::Platypus->attach(...)`. Each generated module is a plain `.pm` that can be
`use`d directly:
**Generated output** (`gen/`):

```perl
use Wx::Gen::Button;
my $btn = Wx::Gen::Button->Create($parent, -1, "OK", ...);
```

---

### Fortran (kwxFortran)

**Prerequisites:** CMake 3.30+ and a C++23 compiler

**Step 1 — Build kwxgen and generate** (from CMakeLists.txt or a script):
- `kwx_ffi_gen.ts` — `Deno.dlopen` call with all C symbol definitions (classes, free functions,
events, keys, constants). Import `lib` from this module to call functions directly.
- `kwx_constants_gen.ts` — Eagerly-evaluated numeric exports for all events, keys, and constants.
- `kwx_free_functions_gen.ts` — TypeScript wrappers for non-class C functions.
- `wx{Class}_gen.ts` — One file per class with a TypeScript class wrapping the FFI calls.
- `kwx_gen.ts` — Master barrel re-export of every generated module.

```sh
cmake -S extern/kwxFFI/tools/kwxgen -B build/kwxgen -G Ninja
cmake --build build/kwxgen
Run with:

build/kwxgen/kwxgen generate \
--headers extern/kwxFFI/include \
--defs extern/kwxFFI/src/kwx_defs.cpp \
--lang fortran \
--out src/gen/
```
```ts
import { lib } from "./gen/kwx_ffi_gen.ts";

**Generated output** (`src/gen/`): one `wx_button.f90`-style `module` per class using
`ISO_C_BINDING` `interface` blocks, plus `wx_events.f90`, `wx_keys.f90`,
`wx_constants.f90`. Compile and `use` normally:
// Or import the barrel for the full class-based API
import "./gen/kwx_gen.ts";

```fortran
use wx_button
use wx_events
// Run with: deno run --allow-ffi your_script.ts
```

---

## Re-generating After kwxFFI Updates

When kwxFFI adds new classes or changes signatures:
Expand Down
1 change: 1 addition & 0 deletions files_list.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ set(KWXGEN_SRC_FILES
src/lang/lang_luajit.cpp # Generates LuaJIT FFI declarations
src/lang/lang_perl.cpp # Generates Perl FFI bindings
src/lang/lang_rust.cpp # Generates Rust FFI bindings
src/lang/lang_typescript.cpp # Generates Deno TypeScript FFI bindings

src/emitter.h # Base class for language-specific code emitters
src/model.h # Data structures representing parsed classes, events, keys, etc.
Expand Down
21 changes: 12 additions & 9 deletions src/lang/lang_go.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,8 @@ static size_t RemoveStaleGoClassFiles(const fs::path& out_dir,
std::ignore = fs::remove(entry.path(), errc);
if (errc)
{
std::println(stderr, "Warning: failed to remove stale generated file {}: {}", entry.path().string(), errc.message());
std::println(stderr, "Warning: failed to remove stale generated file {}: {}",
entry.path().string(), errc.message());
}
else
{
Expand Down Expand Up @@ -319,7 +320,8 @@ static std::vector<GoParam> ConvertParam(const Param& param)
}
else
{
std::println(stderr, "Warning: TArrayString macro_arg '{}' has fewer than 2 components", param.macro_arg);
std::println(stderr, "Warning: TArrayString macro_arg '{}' has fewer than 2 components",
param.macro_arg);
}
return result;
}
Expand All @@ -338,7 +340,8 @@ static std::vector<GoParam> ConvertParam(const Param& param)
}
else
{
std::println(stderr, "Warning: TArrayInt macro_arg '{}' has fewer than 2 components", param.macro_arg);
std::println(stderr, "Warning: TArrayInt macro_arg '{}' has fewer than 2 components",
param.macro_arg);
}
return result;
}
Expand All @@ -357,7 +360,8 @@ static std::vector<GoParam> ConvertParam(const Param& param)
}
else
{
std::println(stderr, "Warning: TByteString macro_arg '{}' has fewer than 2 components", param.macro_arg);
std::println(stderr, "Warning: TByteString macro_arg '{}' has fewer than 2 components",
param.macro_arg);
}
return result;
}
Expand Down Expand Up @@ -1227,7 +1231,8 @@ void GoEmitter::Generate(const ParsedFFI& parsed_ffi, const fs::path& out_dir)
GenerateFreeFunctions(parsed_ffi, out_dir);
const size_t class_file_count = GenerateClassFiles(parsed_ffi, out_dir);

std::println(stderr, "Go: checked {:L} files in {}", kFixedFileCount + class_file_count, out_dir.string());
std::println(stderr, "Go: checked {:L} files in {}", kFixedFileCount + class_file_count,
out_dir.string());
}

VerifyResult GoEmitter::Verify(const ParsedFFI& /* parsed_ffi */, const fs::path& /* directory */)
Expand Down Expand Up @@ -1565,14 +1570,12 @@ size_t GoEmitter::GenerateClassFiles(const ParsedFFI& parsed_ffi, const fs::path

const size_t removed_stale = RemoveStaleGoClassFiles(out_dir, expected_class_files);

std::print(stderr, " class files: {:L} files, {:L} methods",
file_count, method_count);
std::print(stderr, " class files: {:L} files, {:L} methods", file_count, method_count);
if (skipped_methods > 0)
{
std::print(stderr, " ({:L} skipped)", skipped_methods);
}
std::print(stderr, " [{:L} written, {:L} unchanged", written_count,
file_count - written_count);
std::print(stderr, " [{:L} written, {:L} unchanged", written_count, file_count - written_count);
if (removed_stale > 0)
{
std::print(stderr, ", {:L} stale removed", removed_stale);
Expand Down
3 changes: 2 additions & 1 deletion src/lang/lang_julia.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,8 @@ static std::vector<JuliaIdiomParam> ConvertToIdiomParams(const Param& param, boo
const std::vector<std::string> names = JuliaSplitMacroArg(param.macro_arg);
if (names.size() < 2)
{
std::println(stderr, "Warning: {}({}) expected 2 names, got {}", param.macro_name, param.macro_arg, names.size());
std::println(stderr, "Warning: {}({}) expected 2 names, got {}", param.macro_name,
param.macro_arg, names.size());
return result;
}
result.push_back({ names[0] + "::Integer", "Cint(" + names[0] + ")", "", "" });
Expand Down
10 changes: 6 additions & 4 deletions src/lang/lang_luajit.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -494,7 +494,8 @@ void LuaJITEmitter::GenerateConstants(const ParsedFFI& ffi_data, const fs::path&

writer << "]]\n";

std::println(stderr, " kwxffi_constants_gen.lua: {:L} constants", ffi_data.constants.size());
std::println(stderr, " kwxffi_constants_gen.lua: {:L} constants",
ffi_data.constants.size());
}

// -------------------------------------------------------------------------
Expand Down Expand Up @@ -543,8 +544,8 @@ void LuaJITEmitter::GenerateClasses(const ParsedFFI& ffi_data, const fs::path& o
writer << "]]\n\n";
}

std::print(stderr, " kwxffi_classes_gen.lua: {:L} classes, {:L} methods",
class_count, method_count);
std::print(stderr, " kwxffi_classes_gen.lua: {:L} classes, {:L} methods", class_count,
method_count);
if (skipped_count > 0)
{
std::print(stderr, " ({} skipped)", skipped_count);
Expand Down Expand Up @@ -1125,7 +1126,8 @@ void LuaJITEmitter::GenerateIdiomaticClasses(const ParsedFFI& ffi_data, const fs

master << "\nreturn M\n";

std::print(stderr, " kwxffi_idiomatic_gen.lua: {} classes, {} methods", class_count, method_count);
std::print(stderr, " kwxffi_idiomatic_gen.lua: {} classes, {} methods", class_count,
method_count);
if (skipped_count > 0)
{
std::print(stderr, " ({} skipped)", skipped_count);
Expand Down
12 changes: 8 additions & 4 deletions src/lang/lang_perl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -877,7 +877,8 @@ void PerlEmitter::GenerateRawClassFiles(const ParsedFFI& ffi, const fs::path& ra
++classCount;
}

std::println(stderr, " raw/ class modules: {} classes, {} functions", classCount, totalMethods);
std::println(stderr, " raw/ class modules: {} classes, {} functions", classCount,
totalMethods);
}

// =========================================================================
Expand Down Expand Up @@ -982,7 +983,8 @@ void PerlEmitter::GenerateRawConstants(const ParsedFFI& ffi, const fs::path& raw

out << "1;\n";

std::println(stderr, " raw/Constants.pm: {} events, {} keys, {} constants", sortedEvents.size(), sortedKeys.size(), sortedConstants.size());
std::println(stderr, " raw/Constants.pm: {} events, {} keys, {} constants",
sortedEvents.size(), sortedKeys.size(), sortedConstants.size());
}

// =========================================================================
Expand Down Expand Up @@ -1130,7 +1132,8 @@ void PerlEmitter::GenerateRawInit(const ParsedFFI& ffi, const fs::path& rawDir)
out << "}\n\n";
out << "1;\n";

std::println(stderr, " raw/Init.pm: master init ({} modules)", classNames.size());
std::println(stderr, " raw/Init.pm: master init ({} modules)",
classNames.size());
}

// =========================================================================
Expand Down Expand Up @@ -1170,7 +1173,8 @@ void PerlEmitter::GenerateOOClasses(const ParsedFFI& ffi, const fs::path& ooDir)
}
}

std::println(stderr, " wx/ OO modules: {} classes, {} methods", classCount, methodCount);
std::println(stderr, " wx/ OO modules: {} classes, {} methods", classCount,
methodCount);
}

void PerlEmitter::EmitOOClassFile(std::ostream& out, const ClassInfo& class_info,
Expand Down
Loading
Loading