diff --git a/.github/workflows/build-macos-reusable.yml b/.github/workflows/build-macos-reusable.yml index a385444..2e37411 100644 --- a/.github/workflows/build-macos-reusable.yml +++ b/.github/workflows/build-macos-reusable.yml @@ -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 (C++23) and 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 support. + echo "STDLIB_FLAG=-stdlib=libc++" >> $GITHUB_ENV - name: Setup CMake 3.30+ and Ninja uses: lukka/get-cmake@latest @@ -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: | diff --git a/.github/workflows/build-unix-reusable.yml b/.github/workflows/build-unix-reusable.yml index 3387a0e..e3ed826 100644 --- a/.github/workflows/build-unix-reusable.yml +++ b/.github/workflows/build-unix-reusable.yml @@ -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 (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: | diff --git a/README.md b/README.md index 4029612..4bee72c 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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: diff --git a/files_list.cmake b/files_list.cmake index 4917eb9..bd05245 100644 --- a/files_list.cmake +++ b/files_list.cmake @@ -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. diff --git a/src/lang/lang_go.cpp b/src/lang/lang_go.cpp index c817111..c59ceee 100644 --- a/src/lang/lang_go.cpp +++ b/src/lang/lang_go.cpp @@ -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 { @@ -319,7 +320,8 @@ static std::vector 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; } @@ -338,7 +340,8 @@ static std::vector 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; } @@ -357,7 +360,8 @@ static std::vector 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; } @@ -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 */) @@ -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); diff --git a/src/lang/lang_julia.cpp b/src/lang/lang_julia.cpp index 53b1aae..0ce792a 100644 --- a/src/lang/lang_julia.cpp +++ b/src/lang/lang_julia.cpp @@ -271,7 +271,8 @@ static std::vector ConvertToIdiomParams(const Param& param, boo const std::vector 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] + ")", "", "" }); diff --git a/src/lang/lang_luajit.cpp b/src/lang/lang_luajit.cpp index a4f0bea..5529bb7 100644 --- a/src/lang/lang_luajit.cpp +++ b/src/lang/lang_luajit.cpp @@ -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()); } // ------------------------------------------------------------------------- @@ -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); @@ -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); diff --git a/src/lang/lang_perl.cpp b/src/lang/lang_perl.cpp index 9adb444..6169e05 100644 --- a/src/lang/lang_perl.cpp +++ b/src/lang/lang_perl.cpp @@ -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); } // ========================================================================= @@ -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()); } // ========================================================================= @@ -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()); } // ========================================================================= @@ -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, diff --git a/src/lang/lang_typescript.cpp b/src/lang/lang_typescript.cpp new file mode 100644 index 0000000..7aabf47 --- /dev/null +++ b/src/lang/lang_typescript.cpp @@ -0,0 +1,833 @@ +///////////////////////////////////////////////////////////////////////////// +// Purpose: Deno TypeScript FFI binding generator +// Author: Ralph Walden +// Copyright: Copyright (c) 2026 KeyWorks Software (Ralph Walden) +// License: Apache License -- see ../LICENSE +///////////////////////////////////////////////////////////////////////////// + +// Generated file structure: +// kwx_ffi_gen.ts — Deno.dlopen with every C symbol (classes + free functions + +// events/keys/constants). Import `lib` from this module to call +// functions directly via lib.symbols.SomeFunc(...). +// kwx_constants_gen.ts — Eagerly-initialized numeric exports for all events, keys, and +// general constants (calls the FFI accessor functions at load time). +// kwx_free_functions_gen.ts — TypeScript wrappers for non-class C functions. +// wx{Class}_gen.ts — One file per class with a TypeScript class that wraps the FFI. +// kwx_gen.ts — Master barrel re-export of every generated module. + +#include "lang_typescript.h" + +#include "../file_writer.h" +#include "lang_common.h" +#include "typescript_type_map.h" + +#include +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; + +// ------------------------------------------------------------------------- +// Local helpers +// ------------------------------------------------------------------------- + +// Return the TypeScript filename for a class: "wxButton" → "wxButton_gen.ts" +[[nodiscard]] static std::string TsClassFileName(const std::string& class_name) +{ + return class_name + "_gen.ts"; +} + +// Return the TypeScript import path for a class file (Deno resolves .ts automatically). +[[nodiscard]] static std::string TsClassImportPath(const std::string& class_name) +{ + return "./" + class_name + "_gen.ts"; +} + +// True if a non-constructor method's first parameter is TClass(ClassName) matching +// the function's own class — acts as the implicit "self" receiver for mixin methods. +[[nodiscard]] static bool IsPseudoSelf(const FunctionDecl& func) +{ + if (func.has_self || func.class_name.empty() || func.params.empty()) + { + return false; + } + const Param& first = func.params[0]; + return first.macro_name == "TClass" && first.macro_arg == func.class_name; +} + +// True if a destructor has only a TSelf parameter (no item-index or other params). +[[nodiscard]] static bool IsTrueDestructor(const FunctionDecl& func) +{ + if (!func.is_destructor) + { + return false; + } + for (const auto& param: func.params) + { + if (param.macro_name != "TSelf") + { + return false; + } + } + return true; +} + +// Find the nearest wrapped ancestor of class_name by walking the parent_map chain. +// Returns empty string if no wrapped parent exists (class becomes a root class). +[[nodiscard]] static std::string + FindTsWrappedParent(const std::string& class_name, const ParsedFFI& ffi, + const std::unordered_set& wrapped_classes) +{ + std::unordered_map::const_iterator iter = + ffi.parent_map.find(class_name); + std::set visited; + while (iter != ffi.parent_map.end()) + { + const std::string& parent = iter->second; + if (visited.contains(parent)) + { + break; // cycle guard + } + visited.insert(parent); + if (wrapped_classes.contains(parent)) + { + return parent; + } + iter = ffi.parent_map.find(parent); + } + return {}; +} + +// Wraps a single kwx Param into information needed for the idiomatic TS method signature. +// Each returned entry is one visible TypeScript parameter (or a hidden self receiver). +struct WrapParam +{ + std::string name; // TypeScript parameter name (empty when is_self is true) + std::string ts_type; // TypeScript type annotation (e.g., "number", "boolean") + std::string ffi_expr; // Expression to pass to the FFI call (e.g., "flag ? 1 : 0") + bool is_self = false; // True → not visible in TS signature; ffi_expr is "this._ptr" +}; + +[[nodiscard]] static std::vector BuildWrapParams(const Param& param) +{ + std::vector result; + + // TSelf → hidden receiver, pass this._ptr to the C call + if (param.macro_name == "TSelf") + { + result.push_back({ "", "", "this._ptr", true }); + return result; + } + + // TBool/TBoolInt → accept boolean in TS, convert to 1/0 for the C call + if (param.macro_name == "TBool" || param.raw_type == "TBool" || param.raw_type == "TBoolInt" || + param.raw_type == "TBool*") + { + const std::string pname = + TsEscapeName(param.param_name.empty() ? "flag" : param.param_name); + result.push_back({ pname, "boolean", pname + " ? 1 : 0", false }); + return result; + } + + // All other types: pass through the FFI type mapping + const std::vector ffi_params = ExpandParamToTsFFI(param); + for (const auto& fpar: ffi_params) + { + result.push_back({ fpar.name, TsRuntimeType(fpar.deno_type), fpar.name, false }); + } + return result; +} + +// Return the TypeScript return type annotation for a method in the class wrapper layer. +// class_name is the enclosing class (used when return is TClass/TSelf → ClassName | null). +[[nodiscard]] static std::string WrapReturnType(const FunctionDecl& func, + const std::string& class_name) +{ + if (func.return_type == "void" || func.return_type.empty()) + { + return "void"; + } + if (func.return_macro == "TClass" || func.return_macro == "TSelf" || + func.return_macro == "TClassRef") + { + // Constructor: return the enclosing class wrapped in null-check. + // Regular methods returning a pointer: return opaque PointerValue. + if (func.is_constructor && !func.has_self) + { + return class_name + " | null"; + } + return "Deno.PointerValue"; + } + if (func.return_type == "TBool" || func.return_type == "TBoolInt") + { + return "boolean"; + } + if (func.return_type == "TString" || func.return_type == "TStringOut" || + func.return_type == "TStringVoid") + { + return "Deno.PointerValue"; + } + if (func.return_type == "int" || func.return_type == "TArrayLen" || + func.return_type == "TByteStringLen") + { + return "number"; + } + if (func.return_type == "long" || func.return_type == "time_t") + { + return "number"; + } + if (func.return_type == "unsigned" || func.return_type == "unsigned int") + { + return "number"; + } + if (func.return_type == "unsigned long" || func.return_type == "wxUIntPtr") + { + return "number"; + } + if (func.return_type == "uintptr_t" || func.return_type == "size_t") + { + return "bigint"; + } + if (func.return_type == "double" || func.return_type == "float") + { + return "number"; + } + if (func.return_type == "TChar" || func.return_type == "TUInt8") + { + return "number"; + } + if (func.return_type == "void*" || func.return_type.find('*') != std::string::npos) + { + return "Deno.PointerValue"; + } + return "number"; // fallback +} + +// Emit the return statement for a method wrapper, applying any needed conversions. +// call_expr is the raw lib.symbols.Func(...) expression (without trailing semicolon). +static void EmitWrapReturn(std::ostream& output, const FunctionDecl& func, + const std::string& class_name, const std::string& call_expr) +{ + const bool is_void = func.return_type.empty() || func.return_type == "void"; + const bool is_ctor = func.is_constructor && !func.has_self && + (func.return_macro == "TClass" || func.return_macro == "TSelf" || + func.return_macro == "TClassRef"); + const bool is_bool = func.return_type == "TBool" || func.return_type == "TBoolInt"; + const bool is_bigint = func.return_type == "uintptr_t" || func.return_type == "size_t"; + + if (is_void) + { + output << " " << call_expr << ";\n"; + return; + } + + if (is_ctor) + { + output << " const rawPtr = " << call_expr << ";\n"; + output << " if (rawPtr === null) return null;\n"; + output << " return new " << class_name << "(rawPtr);\n"; + return; + } + + if (is_bool) + { + output << " return (" << call_expr << " as number) !== 0;\n"; + return; + } + + if (is_bigint) + { + output << " return " << call_expr << " as bigint;\n"; + return; + } + + output << " return " << call_expr << ";\n"; +} + +// Emit the Deno.dlopen symbol entry for a single function. +static void EmitSymbolDef(std::ostream& output, const FunctionDecl& func) +{ + const std::string func_name = CFuncName(func); + const std::string result_str = TsFFIReturnType(func.return_type, func.return_macro); + + // Build the parameters array + std::vector params; + for (const auto& param: func.params) + { + const std::vector expanded = ExpandParamToTsFFI(param); + for (const auto& ffi_param: expanded) + { + params.push_back(ffi_param); + } + } + + output << " " << func_name << ": {\n"; + output << " parameters: ["; + for (size_t idx = 0; idx < params.size(); ++idx) + { + if (idx > 0) + { + output << ", "; + } + output << params[idx].deno_type; + } + output << "],\n"; + output << " result: " << result_str << ",\n"; + output << " },\n"; +} + +// ------------------------------------------------------------------------- +// TypeScriptEmitter public interface +// ------------------------------------------------------------------------- + +void TypeScriptEmitter::Generate(const ParsedFFI& ffi, const fs::path& out_dir) +{ + if (!fs::create_directories(out_dir)) + { + if (!fs::exists(out_dir)) + { + std::println(stderr, "Error: cannot create output directory {}", out_dir.string()); + return; + } + } + + GenerateFfi(ffi, out_dir); + GenerateConstants(ffi, out_dir); + GenerateFreeFunctions(ffi, out_dir); + GenerateClassFiles(ffi, out_dir); + GenerateIndex(ffi, out_dir); + + std::println(stderr, "TypeScript: generated Deno FFI bindings in {}", out_dir.string()); +} + +VerifyResult TypeScriptEmitter::Verify(const ParsedFFI& /* ffi */, const fs::path& /* out_dir */) +{ + VerifyResult result; + result.success = false; + result.messages.emplace_back("TypeScript verify: use 'kwxgen verify' command instead"); + return result; +} + +// ------------------------------------------------------------------------- +// kwx_ffi_gen.ts — Deno.dlopen with all C symbols +// ------------------------------------------------------------------------- + +void TypeScriptEmitter::GenerateFfi(const ParsedFFI& ffi, const fs::path& out_dir) +{ + const fs::path file_path = out_dir / "kwx_ffi_gen.ts"; + ConditionalFileWriter output(file_path); + + WriteGeneratedHeader(output, "//"); + + // Platform-specific library name + output << "const _libName: string = ({\n"; + output << " windows: \"" << ffi.lib_name << ".dll\",\n"; + output << " linux: \"lib" << ffi.lib_name << ".so\",\n"; + output << " darwin: \"lib" << ffi.lib_name << ".dylib\",\n"; + output << "} as Record)[Deno.build.os] ?? \"" << ffi.lib_name << "\";\n\n"; + + output << "// Run with: deno run --allow-ffi your_script.ts\n"; + output << "export const lib = Deno.dlopen(_libName, {\n"; + + // Class methods + size_t method_count = 0; + for (const auto& cls: ffi.classes) + { + for (const auto& func: cls.methods) + { + if (!IsValidFunction(func)) + { + continue; + } + EmitSymbolDef(output, func); + ++method_count; + } + } + + // Free functions + for (const auto& func: ffi.free_functions) + { + if (!IsValidFunction(func)) + { + continue; + } + EmitSymbolDef(output, func); + } + + // Event accessor symbols (no parameters, return i32) + for (const auto& event: ffi.events) + { + output << " " << event.export_name << ": { parameters: [], result: \"i32\" },\n"; + } + + // Key accessor symbols (no parameters, return i32) + for (const auto& key: ffi.keys) + { + output << " " << key.export_name << ": { parameters: [], result: \"i32\" },\n"; + } + + // Constant accessor symbols (no parameters, return i32 or pointer) + for (const auto& cnst: ffi.constants) + { + const bool is_ptr = cnst.return_type.contains('*'); + const char* result_str = is_ptr ? "\"pointer\"" : "\"i32\""; + output << " " << cnst.export_name << ": { parameters: [], result: " << result_str + << " },\n"; + } + + output << "} as const);\n"; + + std::println(stderr, + " kwx_ffi_gen.ts: {} class methods, {} free functions, " + "{} events, {} keys, {} constants", + method_count, ffi.free_functions.size(), ffi.events.size(), ffi.keys.size(), + ffi.constants.size()); +} + +// ------------------------------------------------------------------------- +// kwx_constants_gen.ts — eagerly-evaluated constants, events, and keys +// ------------------------------------------------------------------------- + +void TypeScriptEmitter::GenerateConstants(const ParsedFFI& ffi, const fs::path& out_dir) +{ + const fs::path file_path = out_dir / "kwx_constants_gen.ts"; + ConditionalFileWriter output(file_path); + + WriteGeneratedHeader(output, "//"); + output << "import { lib } from \"./kwx_ffi_gen.ts\";\n\n"; + + // Events + if (!ffi.events.empty()) + { + output << "// Events\n"; + std::vector sorted_events = ffi.events; + std::ranges::sort(sorted_events, {}, &EventDecl::event_name); + + std::string last_event; + for (const auto& event: sorted_events) + { + if (event.event_name == last_event) + { + continue; + } + last_event = event.event_name; + output << "export const " << event.event_name << ": number = lib.symbols." + << event.export_name << "() as number;\n"; + } + output << "\n"; + } + + // Keys + if (!ffi.keys.empty()) + { + output << "// Keys\n"; + std::vector sorted_keys = ffi.keys; + std::ranges::sort(sorted_keys, {}, &KeyDecl::key_name); + + for (const auto& key: sorted_keys) + { + output << "export const " << key.key_name << ": number = lib.symbols." + << key.export_name << "() as number;\n"; + } + output << "\n"; + } + + // General constants + if (!ffi.constants.empty()) + { + output << "// Constants\n"; + std::vector sorted_consts = ffi.constants; + std::ranges::sort(sorted_consts, {}, &ConstantDecl::constant_name); + + for (const auto& cnst: sorted_consts) + { + const bool is_ptr = cnst.return_type.contains('*'); + if (is_ptr) + { + output << "export const " << cnst.constant_name + << ": Deno.PointerValue = lib.symbols." << cnst.export_name + << "() as Deno.PointerValue;\n"; + } + else + { + output << "export const " << cnst.constant_name << ": number = lib.symbols." + << cnst.export_name << "() as number;\n"; + } + } + } + + std::println(stderr, " kwx_constants_gen.ts: {} events, {} keys, {} constants", + ffi.events.size(), ffi.keys.size(), ffi.constants.size()); +} + +// ------------------------------------------------------------------------- +// kwx_free_functions_gen.ts — free-function wrappers +// ------------------------------------------------------------------------- + +void TypeScriptEmitter::GenerateFreeFunctions(const ParsedFFI& ffi, const fs::path& out_dir) +{ + if (ffi.free_functions.empty()) + { + return; + } + + const fs::path file_path = out_dir / "kwx_free_functions_gen.ts"; + ConditionalFileWriter output(file_path); + + WriteGeneratedHeader(output, "//"); + output << "import { lib } from \"./kwx_ffi_gen.ts\";\n\n"; + + size_t func_count = 0; + for (const auto& func: ffi.free_functions) + { + if (!IsValidFunction(func)) + { + continue; + } + + const std::string func_name = CFuncName(func); + const std::string ret_type = WrapReturnType(func, ""); + + // Build visible parameters + std::vector wrap_params; + for (const auto& param: func.params) + { + const std::vector params = BuildWrapParams(param); + for (const auto& wpar: params) + { + wrap_params.push_back(wpar); + } + } + + output << "export function " << TsEscapeName(func.method_name) << "("; + bool first = true; + for (const auto& wpar: wrap_params) + { + if (wpar.is_self) + { + continue; + } + if (!first) + { + output << ", "; + } + output << wpar.name << ": " << wpar.ts_type; + first = false; + } + output << "): " << ret_type << " {\n"; + std::string call = "lib.symbols." + func_name + "("; + bool call_first = true; + for (const auto& wpar: wrap_params) + { + if (!call_first) + { + call += ", "; + } + call += wpar.ffi_expr; + call_first = false; + } + call += ")"; + + EmitWrapReturn(output, func, "", call); + + output << "}\n\n"; + ++func_count; + } + + std::println(stderr, " kwx_free_functions_gen.ts: {} free functions", func_count); +} + +// ------------------------------------------------------------------------- +// wx{Class}_gen.ts — per-class TypeScript wrapper +// ------------------------------------------------------------------------- + +void TypeScriptEmitter::EmitClassFile(std::ostream& output, const ClassInfo& cls, + const ParsedFFI& ffi) +{ + // Build the set of classes that actually have generated wrapper methods. + std::unordered_set wrapped_classes; + for (const auto& other: ffi.classes) + { + if (!other.methods.empty()) + { + wrapped_classes.insert(other.name); + } + } + + // Find the nearest wrapped ancestor for extends-based inheritance. + const std::string parent_name = FindTsWrappedParent(cls.name, ffi, wrapped_classes); + + WriteGeneratedHeader(output, "//"); + output << "import { lib } from \"./kwx_ffi_gen.ts\";\n"; + if (!parent_name.empty()) + { + output << "import { " << parent_name << " } from \"" << TsClassImportPath(parent_name) + << "\";\n"; + } + output << "\n"; + output << "export class " << cls.name; + if (!parent_name.empty()) + { + output << " extends " << parent_name; + } + output << " {\n"; + if (parent_name.empty()) + { + // Root class (no wrapped parent) — own the pointer field. + output << " protected readonly _ptr: Deno.PointerValue;\n\n"; + output << " constructor(ptr: Deno.PointerValue) {\n"; + output << " this._ptr = ptr;\n"; + output << " }\n\n"; + } + output << " /** Returns the underlying native pointer. */\n"; + output << " get ptr(): Deno.PointerValue {\n"; + output << " return this._ptr;\n"; + output << " }\n\n"; + + // Separate deduplication sets: static and instance methods do not share a namespace. + std::set emitted_static_names; + + // --- Constructors (static factory methods) --- + for (const auto& func: cls.methods) + { + if (!IsValidFunction(func)) + { + continue; + } + if (!func.is_constructor || func.has_self) + { + continue; + } + + const std::string method_name = func.method_name; + if (!emitted_static_names.insert(method_name).second) + { + continue; // skip duplicate + } + + const std::string func_name = CFuncName(func); + const std::string ret_type = WrapReturnType(func, cls.name); + + std::vector wrap_params; + for (const auto& param: func.params) + { + const std::vector params = BuildWrapParams(param); + for (const auto& wpar: params) + { + wrap_params.push_back(wpar); + } + } + output << " static " << TsEscapeName(method_name) << "("; + bool first = true; + for (const auto& wpar: wrap_params) + { + if (wpar.is_self) + { + continue; + } + if (!first) + { + output << ", "; + } + output << wpar.name << ": " << wpar.ts_type; + first = false; + } + output << "): " << ret_type << " {\n"; + + std::string call = "lib.symbols." + func_name + "("; + bool call_first = true; + for (const auto& wpar: wrap_params) + { + if (!call_first) + { + call += ", "; + } + call += wpar.ffi_expr; + call_first = false; + } + call += ")"; + + EmitWrapReturn(output, func, cls.name, call); + + output << " }\n\n"; + } + + // --- Destructor --- + for (const auto& func: cls.methods) + { + if (!IsValidFunction(func) || !IsTrueDestructor(func)) + { + continue; + } + + const std::string func_name = CFuncName(func); + + output << " Delete(): void {\n"; + output << " lib.symbols." << func_name << "(this._ptr);\n"; + output << " }\n\n"; + output << " [Symbol.dispose](): void {\n"; + output << " this.Delete();\n"; + output << " }\n\n"; + break; + } + + // --- Regular methods --- + std::set emitted_instance_names; + for (const auto& func: cls.methods) + { + if (!IsValidFunction(func)) + { + continue; + } + // Skip constructors (emitted above as static) + if (func.is_constructor && !func.has_self) + { + continue; + } + // Skip the true destructor (emitted above) + if (IsTrueDestructor(func)) + { + continue; + } + + if (!emitted_instance_names.insert(func.method_name).second) + { + continue; // skip duplicate overload + } + + const bool pseudo_self = IsPseudoSelf(func); + const std::string func_name = CFuncName(func); + const std::string ret_type = WrapReturnType(func, cls.name); + + // Build wrap params; if pseudo-self, treat first param as hidden self + std::vector wrap_params; + for (size_t pidx = 0; pidx < func.params.size(); ++pidx) + { + const Param& param = func.params[pidx]; + if (pseudo_self && pidx == 0) + { + // First param is TClass(ClassName) acting as self + wrap_params.push_back({ "", "", "this._ptr", true }); + continue; + } + const std::vector params = BuildWrapParams(param); + for (const auto& wpar: params) + { + wrap_params.push_back(wpar); + } + } + output << " " << TsEscapeName(func.method_name) << "("; + bool first = true; + for (const auto& wpar: wrap_params) + { + if (wpar.is_self) + { + continue; + } + if (!first) + { + output << ", "; + } + output << wpar.name << ": " << wpar.ts_type; + first = false; + } + output << "): " << ret_type << " {\n"; + + // Build FFI call expression + std::string call = "lib.symbols." + func_name + "("; + bool call_first = true; + + // TSelf and pseudo-self params are already mapped to this._ptr in BuildWrapParams. + for (const auto& wpar: wrap_params) + { + if (!call_first) + { + call += ", "; + } + call += wpar.ffi_expr; + call_first = false; + } + call += ")"; + + EmitWrapReturn(output, func, cls.name, call); + + output << " }\n\n"; + } + + output << "}\n"; +} + +void TypeScriptEmitter::GenerateClassFiles(const ParsedFFI& ffi, const fs::path& out_dir) +{ + std::vector class_names; + size_t total_methods = 0; + + for (const auto& cls: ffi.classes) + { + if (cls.methods.empty()) + { + continue; + } + + const fs::path file_path = out_dir / TsClassFileName(cls.name); + ConditionalFileWriter output(file_path); + + EmitClassFile(output, cls, ffi); + class_names.push_back(cls.name); + + for (const auto& func: cls.methods) + { + if (IsValidFunction(func)) + { + ++total_methods; + } + } + } + + std::println(stderr, " class files: {} classes, {} methods", class_names.size(), + total_methods); +} + +// ------------------------------------------------------------------------- +// kwx_gen.ts — master barrel re-export +// ------------------------------------------------------------------------- + +void TypeScriptEmitter::GenerateIndex(const ParsedFFI& ffi, const fs::path& out_dir) +{ + const fs::path file_path = out_dir / "kwx_gen.ts"; + ConditionalFileWriter output(file_path); + + WriteGeneratedHeader(output, "//"); + output << "// Master barrel re-export for all generated kwxFFI TypeScript bindings.\n"; + output << "// Usage: import { wxButton, EVT_BUTTON } from \"./kwx_gen.ts\";\n\n"; + + output << "export { lib } from \"./kwx_ffi_gen.ts\";\n"; + output << "export * from \"./kwx_constants_gen.ts\";\n"; + + if (!ffi.free_functions.empty()) + { + output << "export * from \"./kwx_free_functions_gen.ts\";\n"; + } + + // Sort class names for deterministic output + std::vector class_names; + for (const auto& cls: ffi.classes) + { + if (!cls.methods.empty()) + { + class_names.push_back(cls.name); + } + } + std::ranges::sort(class_names); + + output << "\n"; + for (const auto& name: class_names) + { + output << "export { " << name << " } from \"" << TsClassImportPath(name) << "\";\n"; + } + + std::println(stderr, " kwx_gen.ts: {} class exports", class_names.size()); +} diff --git a/src/lang/lang_typescript.h b/src/lang/lang_typescript.h new file mode 100644 index 0000000..06c8d40 --- /dev/null +++ b/src/lang/lang_typescript.h @@ -0,0 +1,38 @@ +//////////////////////////////////////////////////// +// Purpose: Deno TypeScript FFI binding generator +// Author: Ralph Walden +// Copyright: Copyright (c) 2026 KeyWorks Software (Ralph Walden) +// License: Apache License -- see ../LICENSE +//////////////////////////////////////////////////// + +#pragma once + +#include "../emitter.h" + +class TypeScriptEmitter : public LanguageEmitter +{ +public: + void Generate(const ParsedFFI& ffi, const std::filesystem::path& out_dir) override; + [[nodiscard]] VerifyResult Verify(const ParsedFFI& ffi, + const std::filesystem::path& out_dir) override; + [[nodiscard]] std::string_view Name() const override { return "typescript"; } + +private: + // Emits kwx_ffi_gen.ts — the Deno.dlopen call with all C symbol definitions. + static void GenerateFfi(const ParsedFFI& ffi, const std::filesystem::path& out_dir); + + // Emits kwx_constants_gen.ts — eagerly-evaluated events, keys, and constants. + static void GenerateConstants(const ParsedFFI& ffi, const std::filesystem::path& out_dir); + + // Emits kwx_free_functions_gen.ts — free-function TypeScript wrappers. + static void GenerateFreeFunctions(const ParsedFFI& ffi, const std::filesystem::path& out_dir); + + // Emits one TypeScript class file per parsed class, plus the master index. + static void GenerateClassFiles(const ParsedFFI& ffi, const std::filesystem::path& out_dir); + + // Emits kwx_gen.ts — barrel re-export of all generated modules. + static void GenerateIndex(const ParsedFFI& ffi, const std::filesystem::path& out_dir); + + // Emits the body of a single TypeScript class wrapper file. + static void EmitClassFile(std::ostream& output, const ClassInfo& cls, const ParsedFFI& ffi); +}; diff --git a/src/lang/typescript_type_map.h b/src/lang/typescript_type_map.h new file mode 100644 index 0000000..8edf735 --- /dev/null +++ b/src/lang/typescript_type_map.h @@ -0,0 +1,398 @@ +///////////////////////////////////////////////////////////////////////////// +// Purpose: Deno TypeScript FFI type mapping +// Author: Ralph Walden +// Copyright: Copyright (c) 2026 KeyWorks Software (Ralph Walden) +// License: Apache License -- see ..\..\LICENSE +///////////////////////////////////////////////////////////////////////////// + +#pragma once + +// Deno FFI type mapping for TypeScript code generation. +// Maps parsed kwx FFI model types to Deno NativeType strings for Deno.dlopen symbol definitions. +// +// Deno NativeType strings: "void", "i8", "u8", "i16", "u16", "i32", "u32", +// "i64", "u64", "usize", "isize", "f32", "f64", "pointer", "buffer", "function" +// +// JavaScript runtime types from Deno: +// number — i8, u8, i16, u16, i32, u32, f32, f64 +// bigint — i64, u64, usize, isize +// Deno.PointerValue — pointer, function +// BufferSource | null — buffer +// void — void (result only) +// +// Note: C "long" maps to "i32" here. On Windows, C long is 32-bit. +// On POSIX 64-bit, C long is 64-bit. wx long usage (IDs, flags) fits in 32 bits. + +#include "../model.h" + +#include +#include +#include + +// A single Deno FFI parameter: a NativeType string and a TypeScript identifier name. +struct TsFFIParam +{ + std::string deno_type; // "\"i32\"", "\"pointer\"", "\"void\"", etc. + std::string name; // TypeScript-safe parameter name +}; + +// TypeScript / JavaScript keywords that must not be used as identifiers. +inline std::string TsEscapeName(const std::string& name) +{ + if (name == "in" || name == "if" || name == "do" || name == "for" || name == "let" || + name == "new" || name == "try" || name == "var" || name == "case" || name == "else" || + name == "enum" || name == "null" || name == "this" || name == "true" || name == "void" || + name == "with" || name == "break" || name == "catch" || name == "class" || + name == "const" || name == "false" || name == "super" || name == "throw" || + name == "while" || name == "yield" || name == "delete" || name == "export" || + name == "import" || name == "return" || name == "static" || name == "switch" || + name == "typeof" || name == "default" || name == "extends" || name == "finally" || + name == "function" || name == "continue" || name == "debugger" || name == "instanceof" || + name == "from" || name == "of" || name == "type" || name == "interface" || + name == "implements" || name == "private" || name == "protected" || name == "public" || + name == "abstract" || name == "declare" || name == "override" || name == "readonly" || + name == "satisfies" || name == "as" || name == "namespace" || name == "async" || + name == "await") + { + return name + "_"; + } + return name; +} + +// Split a comma-separated macro argument string into individual names: "x, y" → {"x", "y"} +inline std::vector TsSplitMacroArg(const std::string& arg) +{ + std::vector parts; + std::istringstream stream(arg); + std::string part; + while (std::getline(stream, part, ',')) + { + const std::string::size_type start_pos = part.find_first_not_of(" \t"); + const std::string::size_type end_pos = part.find_last_not_of(" \t"); + if (start_pos != std::string::npos) + { + parts.push_back(part.substr(start_pos, end_pos - start_pos + 1ULL)); + } + } + return parts; +} + +// Convert a kwx return type to a Deno NativeType string (quoted, ready to embed in TS source). +// Returns "\"void\"" for void/empty, "\"i32\"" for int, "\"pointer\"" for pointer types, etc. +inline std::string TsFFIReturnType(const std::string& return_type, const std::string& return_macro) +{ + if (return_type == "void" || return_type.empty()) + { + return "\"void\""; + } + if (return_macro == "TClass" || return_macro == "TSelf" || return_macro == "TClassRef") + { + return "\"pointer\""; + } + if (return_type == "TBool" || return_type == "TBoolInt") + { + return "\"i32\""; + } + if (return_type == "TString" || return_type == "TStringOut" || return_type == "TStringVoid") + { + return "\"pointer\""; + } + if (return_type == "int" || return_type == "TArrayLen" || return_type == "TByteStringLen") + { + return "\"i32\""; + } + if (return_type == "long" || return_type == "time_t") + { + return "\"i32\""; + } + if (return_type == "unsigned" || return_type == "unsigned int") + { + return "\"u32\""; + } + if (return_type == "unsigned long" || return_type == "wxUIntPtr") + { + return "\"u32\""; + } + if (return_type == "uintptr_t") + { + return "\"usize\""; + } + if (return_type == "double") + { + return "\"f64\""; + } + if (return_type == "float") + { + return "\"f32\""; + } + if (return_type == "size_t") + { + return "\"usize\""; + } + if (return_type == "TChar") + { + return "\"u8\""; + } + if (return_type == "TUInt8") + { + return "\"u8\""; + } + if (return_type == "void*") + { + return "\"pointer\""; + } + // Any remaining pointer type → pointer + if (return_type.find('*') != std::string::npos) + { + return "\"pointer\""; + } + // Fallback + return "\"i32\""; +} + +// Expand a single kwx Param into one or more Deno FFI TsFFIParam entries. +// Geometry macros expand to multiple scalar params; most types expand to exactly one. +inline std::vector ExpandParamToTsFFI(const Param& param_in) +{ + std::vector result; + + // Geometry expansion macros: TPoint, TSize, TVector → two "i32" params + if (param_in.macro_name == "TPoint" || param_in.macro_name == "TSize" || + param_in.macro_name == "TVector") + { + for (const auto& name_str: TsSplitMacroArg(param_in.macro_arg)) + { + result.push_back({ "\"i32\"", TsEscapeName(name_str) }); + } + return result; + } + + if (param_in.macro_name == "TRect") + { + for (const auto& name_str: TsSplitMacroArg(param_in.macro_arg)) + { + result.push_back({ "\"i32\"", TsEscapeName(name_str) }); + } + return result; + } + + if (param_in.macro_name == "TPointLong" || param_in.macro_name == "TSizeLong" || + param_in.macro_name == "TRectLong" || param_in.macro_name == "TVectorLong") + { + for (const auto& name_str: TsSplitMacroArg(param_in.macro_arg)) + { + result.push_back({ "\"i32\"", TsEscapeName(name_str) }); + } + return result; + } + + // Output geometry: int* output pointers → "pointer" + if (param_in.macro_name == "TPointOut" || param_in.macro_name == "TSizeOut" || + param_in.macro_name == "TRectOut" || param_in.macro_name == "TVectorOut") + { + for (const auto& name_str: TsSplitMacroArg(param_in.macro_arg)) + { + result.push_back({ "\"pointer\"", TsEscapeName(name_str) }); + } + return result; + } + + if (param_in.macro_name == "TPointOutVoid" || param_in.macro_name == "TSizeOutVoid" || + param_in.macro_name == "TRectOutVoid" || param_in.macro_name == "TVectorOutVoid") + { + for (const auto& name_str: TsSplitMacroArg(param_in.macro_arg)) + { + result.push_back({ "\"pointer\"", TsEscapeName(name_str) }); + } + return result; + } + + if (param_in.macro_name == "TSizeOutDouble") + { + for (const auto& name_str: TsSplitMacroArg(param_in.macro_arg)) + { + result.push_back({ "\"pointer\"", TsEscapeName(name_str) }); + } + return result; + } + + if (param_in.macro_name == "TColorRGB") + { + for (const auto& name_str: TsSplitMacroArg(param_in.macro_arg)) + { + result.push_back({ "\"u8\"", TsEscapeName(name_str) }); + } + return result; + } + + // Array types: count (i32) + pointer to data + if (param_in.macro_name == "TArrayString" || param_in.macro_name == "TArrayInt" || + param_in.macro_name == "TByteString" || param_in.macro_name == "TByteStringLazy") + { + const std::vector names = TsSplitMacroArg(param_in.macro_arg); + if (names.size() >= 2ULL) + { + result.push_back({ "\"i32\"", TsEscapeName(names[0]) }); + result.push_back({ "\"pointer\"", TsEscapeName(names[1]) }); + } + return result; + } + + if (param_in.macro_name == "TArrayObjectOutVoid") + { + const std::string pname = param_in.param_name.empty() ? "arr" : param_in.param_name; + result.push_back({ "\"pointer\"", TsEscapeName(pname) }); + return result; + } + + // Single-valued macro types + if (param_in.macro_name == "TClass" || param_in.macro_name == "TSelf" || + param_in.macro_name == "TClassRef") + { + const std::string pname = param_in.param_name.empty() ? "self_" : param_in.param_name; + result.push_back({ "\"pointer\"", TsEscapeName(pname) }); + return result; + } + + if (param_in.macro_name == "TBool" || param_in.raw_type == "TBool") + { + const std::string pname = param_in.param_name.empty() ? "flag" : param_in.param_name; + result.push_back({ "\"i32\"", TsEscapeName(pname) }); + return result; + } + + if (param_in.raw_type == "TBoolInt" || param_in.raw_type == "TBool*") + { + const std::string pname = param_in.param_name.empty() ? "flag" : param_in.param_name; + result.push_back({ "\"i32\"", TsEscapeName(pname) }); + return result; + } + + if (param_in.macro_name == "TClosureFun" || param_in.raw_type == "TClosureFun") + { + const std::string pname = param_in.param_name.empty() ? "func_" : param_in.param_name; + result.push_back({ "\"function\"", TsEscapeName(pname) }); + return result; + } + + if (param_in.raw_type == "TStringVoid" || param_in.macro_name == "TStringVoid") + { + const std::string pname = param_in.param_name.empty() ? "wstr" : param_in.param_name; + result.push_back({ "\"pointer\"", TsEscapeName(pname) }); + return result; + } + + if (param_in.raw_type == "TArrayIntOutVoid" || param_in.raw_type == "TArrayIntPtrOutVoid" || + param_in.raw_type == "TArrayStringOutVoid" || param_in.raw_type == "TByteStringOut" || + param_in.raw_type == "TByteStringLazyOut" || param_in.raw_type == "TArrayObjectOutVoid") + { + const std::string pname = param_in.param_name.empty() ? "arrp" : param_in.param_name; + result.push_back({ "\"pointer\"", TsEscapeName(pname) }); + return result; + } + + if (param_in.raw_type == "TChar") + { + const std::string pname = param_in.param_name.empty() ? "char_" : param_in.param_name; + result.push_back({ "\"u8\"", TsEscapeName(pname) }); + return result; + } + + if (param_in.raw_type == "TUInt8") + { + const std::string pname = param_in.param_name.empty() ? "byte_" : param_in.param_name; + result.push_back({ "\"u8\"", TsEscapeName(pname) }); + return result; + } + + if (param_in.raw_type == "TString") + { + const std::string pname = param_in.param_name.empty() ? "wstr" : param_in.param_name; + result.push_back({ "\"pointer\"", TsEscapeName(pname) }); + return result; + } + + if (param_in.raw_type == "TStringOut") + { + const std::string pname = param_in.param_name.empty() ? "wbuf" : param_in.param_name; + result.push_back({ "\"pointer\"", TsEscapeName(pname) }); + return result; + } + + // Plain C types + const std::string pname = + TsEscapeName(param_in.param_name.empty() ? "arg_" : param_in.param_name); + const std::string& rtype = param_in.raw_type; + + if (rtype.empty() || rtype == "int") + { + result.push_back({ "\"i32\"", pname }); + } + else if (rtype == "long") + { + result.push_back({ "\"i32\"", pname }); + } + else if (rtype == "unsigned" || rtype == "unsigned int") + { + result.push_back({ "\"u32\"", pname }); + } + else if (rtype == "unsigned long" || rtype == "wxUIntPtr") + { + result.push_back({ "\"u32\"", pname }); + } + else if (rtype == "uintptr_t") + { + result.push_back({ "\"usize\"", pname }); + } + else if (rtype == "double") + { + result.push_back({ "\"f64\"", pname }); + } + else if (rtype == "float") + { + result.push_back({ "\"f32\"", pname }); + } + else if (rtype == "size_t") + { + result.push_back({ "\"usize\"", pname }); + } + else if (rtype.find('*') != std::string::npos) + { + result.push_back({ "\"pointer\"", pname }); + } + else + { + result.push_back({ "\"i32\"", pname }); // fallback + } + + return result; +} + +// Map a Deno NativeType string to the corresponding TypeScript runtime type annotation. +inline std::string TsRuntimeType(const std::string& deno_type) +{ + if (deno_type == "\"void\"") + { + return "void"; + } + if (deno_type == "\"i8\"" || deno_type == "\"u8\"" || deno_type == "\"i16\"" || + deno_type == "\"u16\"" || deno_type == "\"i32\"" || deno_type == "\"u32\"" || + deno_type == "\"f32\"" || deno_type == "\"f64\"") + { + return "number"; + } + if (deno_type == "\"i64\"" || deno_type == "\"u64\"" || deno_type == "\"usize\"" || + deno_type == "\"isize\"") + { + return "bigint"; + } + if (deno_type == "\"pointer\"" || deno_type == "\"function\"") + { + return "Deno.PointerValue"; + } + if (deno_type == "\"buffer\"") + { + return "BufferSource | null"; + } + return "unknown"; +} diff --git a/src/main.cpp b/src/main.cpp index 72ccb44..a55751a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -9,10 +9,10 @@ // MSVC uses _MSVC_LANG (more reliable than __cplusplus without /Zc:__cplusplus) // GCC, Clang, and others use __cplusplus #if defined(_MSVC_LANG) - #if _MSVC_LANG < 202302L + #if _MSVC_LANG < 2021001L #error "C++23 or later is required to build this project. Please upgrade your compiler." #endif -#elif __cplusplus < 202302L +#elif (__cplusplus < 202101L) #error "C++23 or later is required to build this project. Please upgrade your compiler." #endif @@ -44,6 +44,7 @@ #include "lang/lang_luajit.h" #include "lang/lang_perl.h" #include "lang/lang_rust.h" +#include "lang/lang_typescript.h" #include "model.h" #include "verify.h" @@ -83,16 +84,22 @@ static void PrintUsage(const char* prog_name) { std::println(stderr, "Usage:"); std::println(stderr, " {} parse --headers --defs [--out ]", prog_name); - std::println(stderr, " {} generate --headers --defs --lang --out [--exports]", prog_name); - std::println(stderr, " {} verify --headers --defs --lang --dir ", prog_name); - std::println(stderr, " Available langs: fortran go julia lua perl rust"); + std::println( + stderr, " {} generate --headers --defs --lang --out [--exports]", + prog_name); + std::println(stderr, " {} verify --headers --defs --lang --dir ", + prog_name); + std::println(stderr, " Available langs: fortran go julia lua perl rust typescript"); std::println(stderr, " {} exports --headers --defs --out ", prog_name); std::println(stderr, " {} diff --headers --manifest ", prog_name); std::println(stderr, " {} langs", prog_name); std::println(stderr, ""); std::println(stderr, "Global options (for generate/verify):"); - std::println(stderr, " --libname Runtime shared-library name in generated bindings (default: kwxFFI)"); - std::println(stderr, " --exports Also generate platform export files (.def/.map/.exp)"); + std::println( + stderr, + " --libname Runtime shared-library name in generated bindings (default: kwxFFI)"); + std::println(stderr, + " --exports Also generate platform export files (.def/.map/.exp)"); } struct Args @@ -125,7 +132,8 @@ static bool LoadConfigFile(Args& args) args, config_path.string(), buffer); if (errc) { - std::println(stderr, "Error reading {}: {}", config_path.string(), glz::format_error(errc, buffer)); + std::println(stderr, "Error reading {}: {}", config_path.string(), + glz::format_error(errc, buffer)); return false; } @@ -200,6 +208,7 @@ static std::vector> CreateEmitters() emitters.push_back(std::make_unique()); emitters.push_back(std::make_unique()); emitters.push_back(std::make_unique()); + emitters.push_back(std::make_unique()); return emitters; }