diff --git a/README.md b/README.md index f42dda914..6aaf45d25 100644 --- a/README.md +++ b/README.md @@ -671,7 +671,58 @@ declare function add(a: s32, b: s32): s32; declare function dist(p: Reference): f64; ``` -Only plain structs, enums and `extern "C"` functions are emitted. Classes, namespace members (`lib::helper`) and functions with C++ linkage (`cxx_only`) are left out, without a warning: their symbol names are mangled, so tslang cannot link to them. To bind a C++ API, expose it through `extern "C"` wrapper functions (with an opaque handle for each class) and run tsbindgen on the header that declares them. +Plain structs, enums and `extern "C"` functions are emitted, and polymorphic classes (below). A class without virtual methods (`lib::Widget`), namespace functions (`lib::helper`) and functions with C++ linkage (`cxx_only`) are left out, without a warning. To bind those, expose them through `extern "C"` wrapper functions (with an opaque handle for each class) and run tsbindgen on the header that declares them. + +#### C++ classes + +A polymorphic class (one with virtual methods), in any namespace, becomes a [`@cpp` class](docs/cpp-classes.md): tslang calls its virtual methods through the object's vtable and its non-virtual methods by their mangled symbols, with no wrapper. clang computes the slots and the symbols for the target's ABI (`--target`), so generate the bindings once per ABI: MSVC and the Itanium ABI (Linux, Android) number the slots differently. + +```cpp +namespace geo { +struct Handle { void *p; Handle(const Handle &); ~Handle(); }; +class Shape { +public: + virtual ~Shape(); + virtual int area() const = 0; + virtual void scale(int by) = 0; + virtual void scale(double by) = 0; + virtual Handle handle() = 0; + int id(int x); +}; +class Square : public Shape { public: int area() const override; virtual bool bigger(const Shape &other) const; int side; }; +} +extern "C" geo::Square *make_square(int side); +``` + +`tsbindgen shapes.hpp --target x86_64-pc-windows-msvc` writes: + +```typescript +@cpp +class Shape { // geo::Shape + @slot(1) area(): s32; + @slot(3) scale(by: s32): void; + @slot(2) scale_2(by: f64): void; + @cppnontrivial + @slot(4) handle(): [p: Opaque | null]; + @linkname("?id@Shape@geo@@QEAAHH@Z") id(x: s32): s32; +} +@cpp +class Square extends Shape { // geo::Square + side: s32; + @slot(5) bigger(other: Shape): boolean; +} +declare function make_square(side: s32): Square; +``` + +- **Slots** are clang's: the destructor (slot 0 under MSVC, 0 and 1 under the Itanium ABI) and methods that aren't public are slots left out. A method overriding a base's is not declared again: the `@cpp` class's override keeps the base's slot. +- **Overloads** get `_2`, `_3`, ... in declaration order, whatever order the ABI puts their slots in. +- **A pointer or a reference** to an object of a bound class is the class; a reference to anything else is a pointer. +- **A record returned by value** is a tuple with its layout, `@cppnontrivial` when it has a destructor or a copy constructor. +- **Fields** are declared when they are where a `@cpp` class puts them (after the vtable pointer and the base's storage). +- **Left out, with a `// skipped:` line:** `inline` methods (no symbol to link to), struct parameters passed by value, and a non-virtual method returning a struct. Static methods and operators are left out silently. +- **Not bound at all:** templates, classes with several bases or a virtual one, and 32-bit targets. A pointer to such a class stays `Opaque`. + +Compile the bindings with `-mm=gc` or `-mm=none`: a `@cpp` class's objects are C++'s. ## Memory models diff --git a/TSLANG_ISSUES.md b/TSLANG_ISSUES.md new file mode 100644 index 000000000..5c7f9c3c6 --- /dev/null +++ b/TSLANG_ISSUES.md @@ -0,0 +1,158 @@ +# tslang issues found while working on native_ts_graphics + +Collected 2026-10-12 on `tslang v0.0-pre-alpha96`, updated the same day against +`tslang v0.0-pre-alpha97` (I:\tslang\tslang.exe, Windows x64, JIT run: `tslang file.ts`). +Every repro below is a complete file that was run. Source project: +https://github.com/ASDAlexander77/native_ts_graphics + +## Status + +| # | Issue | pre-alpha96 | pre-alpha97 | +|---|---|---|---| +| 1 | A derived class without a constructor does not forward its arguments to the base constructor | bug | **fixed** (TypeScriptCompiler#557) | +| 2 | The error for (1) pointed at line 1, column 1 of the base class | bug | **moot**: nothing synthesized fails any more; an explicit bad `super()` is reported at the call | +| 3 | `u64` `<<` shifted by the count modulo 32 | bug | **fixed** | +| 4 | `Ref` field with a `null` initializer rejected | question | **answered**: declare it `Ref \| null` | +| 5 | `@cppvtable` overload naming (C++ overloads need distinct TypeScript names) | open | **addressed** on branch `cppvtable-slot`: `@cpp` + `@slot(N)` (docs/cpp-classes.md) | +| 6 | C `string` return values keep the raw `char*` (no copy) | open | open, not re-checked | +| 7 | `Opaque` compared with `Opaque \| null` crashed the compiler | - | **fixed** by #536 (re-checked with the original drawScene shape) | +| 8 | A ternary of number literals in arithmetic truncated to the other operand's integer type | - | **fixed** by #560 (still wrong on pre-alpha97) | +| 9-12 | Bool const arrays, `Math.max`, `f32` array literal, `let x = 0` | - | fixed (#537, DefaultLib#31, works, #544 warning) | + +Verified downstream: native_ts_graphics dropped its 71 forwarding constructors (PR #49) and rebuilt +everything with pre-alpha97 (all objects deleted first): Windows build clean, CTest 61/61, Android +x86_64 build of 105 APKs clean, emulator smoke test 64/65 (the one failure, `oit_linked_lists`, was a +process that vanished without a crash dump after loading a 3624 x 3624 texture and passed when rerun +alone; no sign of a compiler cause). + +--- + +## Fixed in pre-alpha97 + +### 1. Implicit constructor of a derived class did not forward its arguments + +TypeScript gives a derived class with no constructor an implicit +`constructor(...args) { super(...args); }`. tslang generated `super()` with no arguments: + +```ts +class Base { + constructor(protected a: int, protected b: int = 0) { + } + sum(): int { return this.a + this.b; } +} +class Derived extends Base { + extra = 1; // no constructor +} +function main(): void { + const d = new Derived(2, 3); // TypeScript: fine, prints 5 + console.log(d.sum()); +} +``` + +pre-alpha96: `error: Expected 1-2 arguments, but got 0.` at `class Base {`, then `failed statement`. +pre-alpha97: prints `5`. + +### 2. Diagnostic location + +Was reported at the base class (1:1 of the file in the project's case), not at the derived class +whose implicit constructor was wrong. With (1) fixed the case no longer occurs; a wrong explicit call +is reported correctly on pre-alpha97: + +```ts +class Derived extends Base { + constructor() { + super(); // badsuper.ts:7:9: error: Expected 1-2 arguments, but got 0. + } +} +``` + +The general point (code synthesized by the compiler should report the location of the construct it was +synthesized for) may still be worth keeping in mind for other synthesized members. + +### 3. `u64` shift count + +```ts +function main(): void { + let a: u64 = 1; + let b = a << 40; + console.log(b); // pre-alpha96: 256, pre-alpha97: 1099511627776 + let c: u64 = 1; + c = c << 33; + console.log(c); // pre-alpha96: 2, pre-alpha97: 8589934592 +} +``` + +Not checked on pre-alpha97: `>>`, `>>>`, `i64`, `<<=`. Worth a regression test if the fix did not cover them. + +### 4. `Ref` field initialized with `null` + +`protected argv: Ref = null;` gives `can't cast from 'null' to 'Reference' in 'strict null +mode'`, which is the intended strictness. The nullable spelling works (pre-alpha97): + +```ts +class Holder { + argv: Ref | null = null; +} +``` + +--- + +## Open + +### 5. `@cppvtable`: overload naming + +From the notes on TypeScriptCompiler PR #550 (the `@cppvtable` decorator): C++ overloads of an abstract +interface method need distinct TypeScript names, and the MSVC slot order of overloads must be checked by +hand (`cl /d1reportSingleClassLayout`). A way to name the C++ overload (or a tool that prints the +slot map) would remove that manual step. See `docs/cppvtable-classes.md`. Not re-checked. + +### 6. C `string` return values are not copied + +A C/C++ function declared as returning `string` hands tslang its raw `char*`, with no copy and no +lifetime tracking. A C helper that reused one `std::string` buffer for several calls returned garbage +to TypeScript later; the project now returns pointers to storage that lives as long as the owning object. +Decide whether this is by design; if so, the C interop docs should say the string must outlive its use, +otherwise copying on return (or an explicit borrowed annotation) would be safer. Not re-checked. + +--- + +## 8. A union of literals in arithmetic (found live on pre-alpha97) + +`c ? 0.5 : 1.0` has the type `0.5 | 1`, a union of literals. A binary operator cast that union to +the other operand's type, so with an integer on the other side the fraction was truncated: + +```ts +function main(): void { + let c = true; + const width = 100; // s32 + console.log(width * (c ? 0.5 : 1.0)); // pre-alpha97: 0, expected 50 + const h = c ? 0.5 : 1.0; + console.log(width * h); // pre-alpha97: 0 + const left = c ? 448.0 : 96.0; + let m = left * 2; + m = m / 3; + console.log(m); // pre-alpha97: 298, expected 298.667 +} +``` + +MLIR: `ts.Cast (!ts.union, !ts.literal<1.0>>) -> si32`, then an s32 multiply. +Fixed by #560: `unwrapForBinaryOp` unwraps a union whose members are +all literals of one type to that type, as it already did for a single literal. Regression test +`tslang/test/tester/tests/00literal_union_arith.ts`. + +--- + +## Older reports (history) + +Seen in real code on tslang versions up to pre-alpha93/94. TypeScriptCompiler's own findings +document (removed by #558, in the history) records #536, #537, #544 and DefaultLib#31 as their fixes; +the ternary one (8 above) was the only one still live. + +| # | Symptom (as first seen) | Minimal repro tried | Result | +|---|---|---|---| +| 7 | Silent compiler crash, exit 0xC00000FD (stack overflow, no message), on `opaque != nullableOpaque` inside a method (`examples/async_compute_bloom.ts` `drawScene`, 2026-10-08) | class `Foo`; `p != this.cur` with `cur: Foo \| null` | fixed by #536; the original shape (`Opaque` ternary vs an `Opaque \| null` local in a loop) compiles and runs correctly on pre-alpha97 | +| 8 | Ternary of numeric literal consts inside arithmetic evaluated wrong, silently (`width * (split ? 0.5 : 1.0)` gave 0) | consts `HALF`/`FULL`, `width * (split ? HALF : FULL) + ((held ? 1.0 : 0.0) - (split ? 1.0 : 0.0))` | correct there because the consts were folded; with a runtime condition it was still wrong: see 8 above | +| 9 | namespace-level `const X = [false, true, ...]` read `false` for every element in `if (X[i])` | same shape, 4 elements, loop | correct | +| 10 | `Math.max` of all-negative numbers returned 2.2e-308 (seeded with `Number.MIN_VALUE`) | `Math.max(-1.0, -2.0)` | `-1`: fixed | +| 11 | array literal mixing an `f32` element and number literals, passed as `number[]`, failed to cast | `[f, 0, 2]` assigned to `number[]` | works | +| 12 | `let x = 0;` later assigned a fraction silently kept an `s32` | `let x = 0; x = 2.5;` | now a clear warning ("a 'number' is stored in a 's32'..."): fixed | diff --git a/docs/cpp-classes.md b/docs/cpp-classes.md index 0f8663561..7f3e5d36a 100644 --- a/docs/cpp-classes.md +++ b/docs/cpp-classes.md @@ -86,6 +86,11 @@ A generic or static class can't be `@cpp` either. ## Writing a binding +`tsbindgen` writes the binding from the C++ header, with the slots, the mangled symbols and the +`@cppnontrivial` marks clang computes for the target (README, "C++ classes"): +`tsbindgen nvrhi.hpp --target x86_64-pc-windows-msvc --filter 'nvrhi::I*' -- -std=c++17`. +Writing one by hand: + - **Slot numbers.** Take them from the compiler, not by counting: MSVC prints a class's vtable with `cl /c /Zs /d1reportSingleClassLayout file.cpp`; clang (Itanium) with `clang++ -S -emit-llvm -o /dev/null -Xclang -fdump-vtable-layouts file.cpp`, for a class whose diff --git a/tslang/include/TsClangImporter/BindingModel.h b/tslang/include/TsClangImporter/BindingModel.h index 44999f2a3..a0ccbbeea 100644 --- a/tslang/include/TsClangImporter/BindingModel.h +++ b/tslang/include/TsClangImporter/BindingModel.h @@ -16,12 +16,14 @@ struct TsType { Builtin, Named, - Reference + Reference, + Tuple // an inline named tuple, `[a: T1, b: T2]`: a C++ record a method returns by value }; Kind kind = Kind::Builtin; std::string name; // Builtin: the tslang spelling; Named: the key of a Decl - std::vector pointee; // Reference: exactly one element + std::vector pointee; // Reference: exactly one element; Tuple: the field types + std::vector fieldNames; // Tuple std::string comment; // shown next to the type: a function pointer's C signature, ... bool functionPointer = false; std::string skipReason; // non-empty: the C type has no tslang mapping @@ -34,6 +36,7 @@ struct TsType static TsType builtin(std::string name, std::string comment = {}); static TsType named(std::string key); static TsType reference(TsType pointee); + static TsType tuple(std::vector names, std::vector types); static TsType skip(std::string reason); }; @@ -73,9 +76,23 @@ enum class DeclKind Enum, // enum E { A = 0 } FixedEnum, // type E = u8; const A = 1; Typedef, // type T = ...; + CppClass, // @cpp class C extends B { @slot(N) f(...): R; @linkname("...") g(...): R; } Function // declare function f(...): R; }; +// A method of a C++ class bound as a @cpp class. +struct Method +{ + std::string name; // the TS name: the C++ name, `_2`, `_3`, ... for later overloads + std::string cppName; // the C++ name, for messages + int slot = -1; // a virtual method's vtable slot; -1 for a non-virtual one + std::string symbol; // a non-virtual method's mangled symbol + std::vector parameters; + TsType result; + bool nonTrivialReturn = false; // returns a record with a destructor or a copy constructor + std::string skipReason; // non-empty: left out, with a `// skipped:` line in its place +}; + struct Decl { DeclKind kind = DeclKind::Function; @@ -86,12 +103,17 @@ struct Decl bool inSystemHeader = false; std::string skipReason; // non-empty: left out, with a `// skipped:` line in its place - std::vector fields; // Struct: fields; Function: parameters + std::vector fields; // Struct, CppClass: fields; Function: parameters TsType type; // Function: result; Typedef: aliased type; FixedEnum: underlying bool varargs = false; // Function std::vector enumerators; // Enum, FixedEnum bool scoped = false; // FixedEnum from an `enum class` std::string value; // Macro: a TS literal + + std::string qualifiedName; // CppClass: the C++ name with its namespaces (`nvrhi::IDevice`) + std::string base; // CppClass: the key of the base class, or empty + std::vector methods; // CppClass: the methods it introduces, in declaration order + std::string fieldsSkipReason; // CppClass: non-empty when its fields aren't declared }; struct HeaderModel diff --git a/tslang/include/TsClangImporter/TypeMapper.h b/tslang/include/TsClangImporter/TypeMapper.h index 68f37bdf9..19f83533b 100644 --- a/tslang/include/TsClangImporter/TypeMapper.h +++ b/tslang/include/TsClangImporter/TypeMapper.h @@ -5,6 +5,11 @@ #include "clang/AST/ASTContext.h" #include "clang/AST/Decl.h" + +namespace clang +{ +class Sema; +} #include "llvm/ADT/DenseMap.h" #include "llvm/ADT/StringSet.h" @@ -58,6 +63,26 @@ class TypeMapper // namespace or a class, and not a template or a template specialization (std::cmatch). bool isDeclarable(const clang::RecordDecl *record); + // Why a C++ class can't be a @cpp class ("" if it can): it has to be polymorphic, not a + // template, on a 64-bit target, with at most one base, itself bindable and not virtual - the + // layout a @cpp class has (the vtable pointer first, a base object the prefix of a derived one). + std::string classSkipReason(const clang::CXXRecordDecl *record); + + // "class:nvrhi::IDevice" + static std::string classKey(const clang::CXXRecordDecl *record); + + // A C++ record returned by value, as a method declared to return a tuple returns it: the + // record's fields, laid out as a tuple's, a pointer as `Opaque | null`. `nonTrivial` is set + // when C++ can't pass it in registers (a destructor or a copy constructor: @cppnontrivial). + TsType mapRecordResult(clang::QualType type, bool &nonTrivial); + + // clang's semantic analysis, to complete a class template specialization only ever named as a + // result (`RefCountPtr createBuffer(...)` doesn't instantiate it) + void setSema(clang::Sema *value) + { + sema = value; + } + // Every struct and enum a mapped type has named, by key. The AST traversal does not reach a // struct first named inside a prototype (`void f(struct S *s);`), so the parser adds these. const std::vector> &referencedTags() const @@ -67,7 +92,7 @@ class TypeMapper private: std::string referTo(const clang::TagDecl *tag); - TsType mapPointer(const clang::PointerType *pointer, Use use); + TsType mapPointer(clang::QualType pointee, Use use); TsType mapBuiltin(clang::QualType canonical, Use use); std::string signatureComment(const clang::FunctionProtoType *function); bool isSystem(const clang::Decl *decl); @@ -75,7 +100,9 @@ class TypeMapper bool leadsToOwner(clang::QualType type); clang::ASTContext &context; + clang::Sema *sema = nullptr; llvm::DenseMap recordReasons; + llvm::DenseMap classReasons; std::vector> referenced; llvm::StringSet<> referencedKeys; std::vector owners; // structs whose fields are being mapped diff --git a/tslang/lib/TsClangImporter/BindingModel.cpp b/tslang/lib/TsClangImporter/BindingModel.cpp index a301ce144..dc663707f 100644 --- a/tslang/lib/TsClangImporter/BindingModel.cpp +++ b/tslang/lib/TsClangImporter/BindingModel.cpp @@ -30,6 +30,15 @@ TsType TsType::reference(TsType pointee) return type; } +TsType TsType::tuple(std::vector names, std::vector types) +{ + TsType type; + type.kind = Kind::Tuple; + type.fieldNames = std::move(names); + type.pointee = std::move(types); + return type; +} + TsType TsType::skip(std::string reason) { TsType type; @@ -47,6 +56,15 @@ std::string renderType(const TsType &type, const std::function"; + case TsType::Kind::Tuple: { + std::string fields; + for (size_t index = 0; index < type.pointee.size(); ++index) + { + fields += (index ? ", " : "") + type.fieldNames[index] + ": " + renderType(type.pointee[index], nameOf); + } + + return "[" + fields + "]"; + } } return type.name; diff --git a/tslang/lib/TsClangImporter/BindingPrinter.cpp b/tslang/lib/TsClangImporter/BindingPrinter.cpp index f6d624089..129ce11ad 100644 --- a/tslang/lib/TsClangImporter/BindingPrinter.cpp +++ b/tslang/lib/TsClangImporter/BindingPrinter.cpp @@ -34,8 +34,9 @@ std::vector selectDecls(const HeaderModel &model, const std::vector selectDecls(const HeaderModel &model, const std::vector printBindings(const HeaderModel &model, const PrintOptions &options) @@ -173,7 +198,8 @@ llvm::Expected printBindings(const HeaderModel &model, const PrintO return text; }; - for (int pass = 0; pass < 3; ++pass) + auto hasCppClass = false; + for (int pass = 0; pass < groups; ++pass) { for (size_t index = 0; index < model.decls.size(); ++index) { @@ -202,6 +228,51 @@ llvm::Expected printBindings(const HeaderModel &model, const PrintO case DeclKind::Macro: line(exported + "const " + tsName + " = " + decl.value + ";"); break; + case DeclKind::CppClass: { + hasCppClass = true; + line("@cpp"); + line(exported + "class " + tsName + (decl.base.empty() ? "" : " extends " + nameOf(decl.base)) + + " { // " + decl.qualifiedName); + auto member = [&](const std::string &text) { line(" " + text); }; + if (!decl.fieldsSkipReason.empty()) + { + member("// fields not declared: " + decl.fieldsSkipReason); + } + + for (auto &field : decl.fields) + { + member(withComment(field) + ";"); + } + + for (auto &method : decl.methods) + { + if (!method.skipReason.empty()) + { + member("// skipped: " + method.cppName + " \xE2\x80\x94 " + method.skipReason); + result.warnings.push_back("skipped " + decl.qualifiedName + "::" + method.cppName + ": " + + method.skipReason); + continue; + } + + std::string parameters; + for (auto ¶meter : method.parameters) + { + parameters += (parameters.empty() ? "" : ", ") + withComment(parameter); + } + + if (method.nonTrivialReturn) + { + member("@cppnontrivial"); + } + + auto decorator = method.slot >= 0 ? "@slot(" + std::to_string(method.slot) + ") " + : "@linkname(\"" + method.symbol + "\") "; + member(decorator + method.name + "(" + parameters + "): " + renderType(method.result, nameOf) + ";"); + } + + line("}"); + break; + } case DeclKind::OpaqueStruct: line(exported + "type " + tsName + " = Opaque;"); break; @@ -279,6 +350,12 @@ llvm::Expected printBindings(const HeaderModel &model, const PrintO result.text += "// A function pointer is Opaque: pass a function without captures, as `fn as Opaque`.\n"; } + if (hasCppClass) + { + result.text += "// The @cpp classes are this target's C++ ABI (generate them again for another); compile with " + "-mm=gc or -mm=none.\n"; + } + if (!result.text.empty()) { result.text += "\n"; diff --git a/tslang/lib/TsClangImporter/HeaderParser.cpp b/tslang/lib/TsClangImporter/HeaderParser.cpp index 2a9a972a1..969d928e9 100644 --- a/tslang/lib/TsClangImporter/HeaderParser.cpp +++ b/tslang/lib/TsClangImporter/HeaderParser.cpp @@ -3,7 +3,12 @@ #include "clang/AST/ASTConsumer.h" #include "clang/AST/DeclCXX.h" +#include "clang/AST/GlobalDecl.h" +#include "clang/AST/Mangle.h" +#include "clang/AST/RecordLayout.h" #include "clang/AST/RecursiveASTVisitor.h" +#include "clang/AST/VTableBuilder.h" +#include "clang/Basic/TargetInfo.h" #include "clang/Basic/DiagnosticOptions.h" #include "clang/Basic/FileManager.h" #include "clang/Frontend/CompilerInstance.h" @@ -13,9 +18,13 @@ #include "clang/Lex/MacroInfo.h" #include "clang/Lex/PPCallbacks.h" #include "clang/Lex/Preprocessor.h" +#include "clang/Sema/SemaConsumer.h" #include "clang/Tooling/Tooling.h" +#include "llvm/ADT/DenseMap.h" #include "llvm/ADT/SmallString.h" +#include "llvm/ADT/StringMap.h" #include "llvm/ADT/StringSet.h" +#include "llvm/Support/MathExtras.h" #include "llvm/Support/MemoryBuffer.h" #include "llvm/Support/VirtualFileSystem.h" @@ -218,14 +227,29 @@ class MacroCollector : public clang::PPCallbacks class Collector : public clang::RecursiveASTVisitor { public: - Collector(clang::ASTContext &context, HeaderModel &model) : context(context), mapper(context), model(model) + Collector(clang::ASTContext &context, HeaderModel &model, clang::Sema *sema) + : context(context), mapper(context), model(model) { + mapper.setSema(sema); } bool VisitRecordDecl(clang::RecordDecl *record) { + if (record->isImplicit()) + { + return true; + } + + // a polymorphic C++ class, in any namespace: a @cpp class + if (auto *cxxRecord = llvm::dyn_cast(record); + cxxRecord && cxxRecord->getDefinition() && mapper.classSkipReason(cxxRecord).empty()) + { + addClass(cxxRecord->getDefinition()); + return true; + } + // the mapper spells a pointer to anything else as plain Opaque, so nothing names it - if (record->isImplicit() || !mapper.isDeclarable(record)) + if (!mapper.isDeclarable(record)) { return true; } @@ -440,6 +464,220 @@ class Collector : public clang::RecursiveASTVisitor } private: + // A @cpp class: its base, its fields when they can be declared, and the methods it introduces. + // A method overriding a base's virtual method isn't one: the @cpp class's override keeps the + // base's slot by name. Destructors, and methods that aren't public, are slots left out (a @cpp + // class may skip slots). + void addClass(const clang::CXXRecordDecl *definition) + { + auto key = TypeMapper::classKey(definition); + if (!keys.insert(key).second) + { + return; + } + + Decl decl; + decl.kind = DeclKind::CppClass; + decl.key = key; + decl.name = definition->getName().str(); + decl.qualifiedName = definition->getQualifiedNameAsString(); + locate(decl, definition); + + const clang::CXXRecordDecl *base = nullptr; + if (definition->getNumBases() == 1) + { + base = definition->bases_begin()->getType()->getAsCXXRecordDecl()->getDefinition(); + addClass(base); // declared before its use, which C++ guarantees but a referenced class may not be + decl.base = TypeMapper::classKey(base); + } + + llvm::StringSet<> inherited; + if (base) + { + inherited = classNames[decl.base]; + } + + auto &usedNames = classNames[key]; + usedNames = std::move(inherited); + + addClassFields(decl, definition, base); + for (auto &field : decl.fields) + { + usedNames.insert(field.name); + } + + for (auto *method : definition->methods()) + { + if (method->isImplicit() || method->isDeleted() || method->isStatic() || + llvm::isa(method) || + method->isOverloadedOperator() || method->getDescribedFunctionTemplate() || + method->getAccess() != clang::AS_public || + (method->isVirtual() && method->size_overridden_methods() > 0)) + { + continue; + } + + Method bound; + bound.cppName = method->getName().str(); + bound.name = escapeReserved(bound.cppName); + for (auto overload = 2; usedNames.count(bound.name); ++overload) + { + bound.name = escapeReserved(bound.cppName) + "_" + std::to_string(overload); + } + + usedNames.insert(bound.name); + mapMethod(bound, method); + decl.methods.push_back(std::move(bound)); + } + + model.decls.push_back(std::move(decl)); + } + + // A class's own fields, after the vtable pointer and its base's fields, as a @cpp class lays + // them out: like a tuple's, the base's storage first, padded to its alignment. Left out (and so + // in every class derived from it) when C++ puts them elsewhere - the Itanium ABI puts a derived + // class's fields in its base's tail padding - or one can't be mapped. + void addClassFields(Decl &decl, const clang::CXXRecordDecl *definition, const clang::CXXRecordDecl *base) + { + uint64_t offset = context.getTargetInfo().getPointerWidth(clang::LangAS::Default); + uint64_t maxAlign = context.getTargetInfo().getPointerAlign(clang::LangAS::Default); + if (base) + { + auto found = fieldsEnd.find(base); + if (found == fieldsEnd.end()) + { + decl.fieldsSkipReason = "the fields of the base class aren't declared"; + return; + } + + offset = found->second.first; + maxAlign = found->second.second; + } + + const auto &layout = context.getASTRecordLayout(definition); + std::vector fields; + unsigned index = 0; + for (auto *field : definition->fields()) + { + auto fieldName = field->getName().str(); + auto reason = [&]() -> std::string { + if (field->isBitField() || fieldName.empty()) + { + return "a bitfield or an anonymous member"; + } + + auto type = mapper.map(field->getType(), TypeMapper::Use::Field); + if (type.skipped()) + { + return "field '" + fieldName + "': " + type.skipReason; + } + + auto align = context.getTypeAlign(field->getType()); + offset = llvm::alignTo(offset, align); + if (layout.getFieldOffset(index) != offset) + { + return "field '" + fieldName + "' isn't where a tuple would put it"; + } + + offset += context.getTypeSize(field->getType()); + maxAlign = std::max(maxAlign, align); + fields.push_back({escapeReserved(fieldName), type}); + return ""; + }(); + + if (!reason.empty()) + { + decl.fieldsSkipReason = reason; + return; + } + + ++index; + } + + decl.fields = std::move(fields); + fieldsEnd[definition] = {llvm::alignTo(offset, maxAlign), maxAlign}; + } + + void mapMethod(Method &bound, const clang::CXXMethodDecl *method) + { + if (method->isVariadic()) + { + bound.skipReason = "variadic method"; + return; + } + + if (method->isVirtual()) + { + auto *vtables = context.getVTableContext(); + if (auto *microsoft = llvm::dyn_cast(vtables)) + { + auto location = microsoft->getMethodVFTableLocation(clang::GlobalDecl(method)); + if (location.VBase || !location.VFPtrOffset.isZero()) + { + bound.skipReason = "its vtable isn't the object's first"; + return; + } + + bound.slot = static_cast(location.Index); + } + else + { + bound.slot = static_cast( + llvm::cast(vtables)->getMethodVTableIndex(clang::GlobalDecl(method))); + } + } + else + { + // defined in the class (or `inline`): no symbol unless something made one + if (method->isInlined()) + { + bound.skipReason = "inline method: no symbol to link against"; + return; + } + + std::unique_ptr mangler(context.createMangleContext()); + llvm::raw_string_ostream symbol(bound.symbol); + mangler->mangleName(clang::GlobalDecl(method), symbol); + symbol.flush(); + } + + for (unsigned index = 0; index < method->getNumParams(); ++index) + { + auto *parameter = method->getParamDecl(index); + auto parameterName = parameter->getName().empty() ? "p" + std::to_string(index) + : escapeReserved(parameter->getName().str()); + auto type = mapper.map(parameter->getType(), TypeMapper::Use::Parameter); + if (type.skipped()) + { + bound.skipReason = "parameter '" + parameterName + "': " + type.skipReason; + return; + } + + bound.parameters.push_back({parameterName, type}); + } + + auto resultType = method->getReturnType(); + if (auto *record = resultType.getCanonicalType()->getAsRecordDecl()) + { + if (!method->isVirtual()) + { + bound.skipReason = "a non-virtual method returning a record by value isn't supported yet"; + return; + } + + bound.result = mapper.mapRecordResult(resultType, bound.nonTrivialReturn); + } + else + { + bound.result = mapper.map(resultType, TypeMapper::Use::Result); + } + + if (bound.result.skipped()) + { + bound.skipReason = "result: " + bound.result.skipReason; + } + } + void completeRecord(size_t index, const clang::RecordDecl *record) { auto *definition = record->getDefinition(); @@ -496,24 +734,40 @@ class Collector : public clang::RecursiveASTVisitor HeaderModel &model; llvm::StringSet<> keys; std::vector> pendingRecords; + // a @cpp class's TS member names, its base's included: a later overload gets `_2`, `_3`, ... + llvm::StringMap> classNames; + // where a @cpp class's declared fields end, padded to its alignment, and that alignment (bits): + // a derived class's fields follow + llvm::DenseMap> fieldsEnd; }; -class ImportConsumer : public clang::ASTConsumer +class ImportConsumer : public clang::SemaConsumer { public: explicit ImportConsumer(HeaderModel &model) : model(model) { } + void InitializeSema(clang::Sema &value) override + { + sema = &value; + } + + void ForgetSema() override + { + sema = nullptr; + } + void HandleTranslationUnit(clang::ASTContext &context) override { - Collector collector(context, model); + Collector collector(context, model, sema); collector.TraverseDecl(context.getTranslationUnitDecl()); collector.finish(); } private: HeaderModel &model; + clang::Sema *sema = nullptr; }; class ImportAction : public clang::ASTFrontendAction diff --git a/tslang/lib/TsClangImporter/TypeMapper.cpp b/tslang/lib/TsClangImporter/TypeMapper.cpp index 70ccd0061..959052340 100644 --- a/tslang/lib/TsClangImporter/TypeMapper.cpp +++ b/tslang/lib/TsClangImporter/TypeMapper.cpp @@ -4,6 +4,8 @@ #include "clang/AST/DeclTemplate.h" #include "clang/AST/RecordLayout.h" #include "clang/Basic/SourceManager.h" +#include "clang/Basic/TargetInfo.h" +#include "clang/Sema/Sema.h" #include "llvm/ADT/SmallPtrSet.h" #include "llvm/Support/MathExtras.h" @@ -86,10 +88,196 @@ bool TypeMapper::isPlainEnum(const clang::EnumDecl *enumDecl) (!enumDecl->isFixed() || canonical->isSpecificBuiltinType(clang::BuiltinType::Int)); } +std::string TypeMapper::classKey(const clang::CXXRecordDecl *record) +{ + return "class:" + record->getQualifiedNameAsString(); +} + +std::string TypeMapper::classSkipReason(const clang::CXXRecordDecl *record) +{ + auto *definition = record->getDefinition(); + if (!definition) + { + return "incomplete class"; + } + + auto found = classReasons.find(definition); + if (found != classReasons.end()) + { + return found->second; + } + + auto reason = [&]() -> std::string { + if (!definition->isPolymorphic()) + { + return "not polymorphic"; + } + + if (definition->getDescribedClassTemplate() || llvm::isa(definition) || + definition->isDependentContext()) + { + return "class template"; + } + + if (definition->getName().empty() || definition->isLocalClass()) + { + return "anonymous or local class"; + } + + if (context.getTargetInfo().getPointerWidth(clang::LangAS::Default) != 64) + { + return "a 32-bit target"; + } + + if (definition->getNumVBases() > 0) + { + return "virtual base class"; + } + + if (definition->getNumBases() > 1) + { + return "multiple inheritance"; + } + + if (definition->getNumBases() == 1) + { + auto *base = definition->bases_begin()->getType()->getAsCXXRecordDecl(); + if (!base) + { + return "base class is a template parameter"; + } + + auto baseReason = classSkipReason(base); + if (!baseReason.empty()) + { + return "base class '" + base->getQualifiedNameAsString() + "': " + baseReason; + } + } + + return ""; + }(); + + classReasons[definition] = reason; + return reason; +} + +TsType TypeMapper::mapRecordResult(clang::QualType type, bool &nonTrivial) +{ + auto *record = type.getCanonicalType()->getAsRecordDecl(); + if (!record->getDefinition() && sema) + { + sema->isCompleteType(clang::SourceLocation(), type.getCanonicalType()); + } + + auto *definition = record->getDefinition(); + if (!definition) + { + return TsType::skip("incomplete record returned by value"); + } + + auto *cxxRecord = llvm::dyn_cast(definition); + nonTrivial = !definition->canPassInRegisters(); + if (definition->isUnion() || (cxxRecord && (cxxRecord->isPolymorphic() || cxxRecord->getNumBases() > 0))) + { + return TsType::skip("record returned by value: a union, a polymorphic or a derived class"); + } + + // the fields as a named tuple lays them out: natural alignment, field after field + const auto &layout = context.getASTRecordLayout(definition); + std::vector names; + std::vector types; + uint64_t offset = 0; + uint64_t maxAlign = 8; + unsigned index = 0; + auto integerRegisterFields = true; + for (auto *field : definition->fields()) + { + auto fieldName = field->getName().str(); + auto fieldType = field->getType().getCanonicalType(); + + // a union of 8-byte members, such as nvrhi::Object's `union { uint64_t integer; void* pointer; }`: + // one 8-byte field, named as its first member + auto *unionRecord = fieldType->getAsRecordDecl(); + auto pointerUnion = false; + if (unionRecord && unionRecord->isUnion() && unionRecord->getDefinition() && context.getTypeSize(fieldType) == 64) + { + auto members = unionRecord->getDefinition()->fields(); + auto eightBytes = !members.empty() && llvm::all_of(members, [&](const clang::FieldDecl *member) { + auto memberType = member->getType().getCanonicalType(); + return !member->isBitField() && !member->getName().empty() && + (memberType->isPointerType() || (memberType->isIntegerType() && context.getTypeSize(memberType) == 64)); + }); + if (eightBytes) + { + pointerUnion = true; + if (fieldName.empty()) + { + fieldName = (*members.begin())->getName().str(); + } + } + } + + if (field->isBitField() || fieldName.empty()) + { + return TsType::skip("record returned by value: a bitfield or an anonymous member"); + } + + TsType type; + if (pointerUnion || fieldType->isPointerType() || fieldType->isReferenceType()) + { + type = TsType::builtin("Opaque | null"); + } + else + { + type = map(field->getType(), Use::Field); + if (type.skipped()) + { + return TsType::skip("record returned by value: field '" + fieldName + "': " + type.skipReason); + } + + integerRegisterFields &= fieldType->isIntegerType() && context.getTypeSize(fieldType) == 64; + } + + auto align = context.getTypeAlign(field->getType()); + offset = llvm::alignTo(offset, align); + if (layout.getFieldOffset(index) != offset) + { + return TsType::skip("record returned by value: packed or over-aligned layout"); + } + + offset += context.getTypeSize(field->getType()); + maxAlign = std::max(maxAlign, align); + names.push_back(escapeReserved(fieldName)); + types.push_back(type); + ++index; + } + + if (names.empty() || llvm::alignTo(offset, maxAlign) != static_cast(context.toBits(layout.getSize()))) + { + return TsType::skip("record returned by value: empty, packed or over-aligned"); + } + + // MSVC returns any record through a hidden pointer, and so does the Itanium ABI one it can't + // pass in registers; a trivial one comes back in registers by its fields, which tslang does + // for one or two 8-byte integer or pointer fields, on x86-64 and AArch64 + if (!context.getTargetInfo().getCXXABI().isMicrosoft() && !nonTrivial) + { + auto arch = context.getTargetInfo().getTriple().getArch(); + if ((arch != llvm::Triple::x86_64 && arch != llvm::Triple::aarch64) || names.size() > 2 || !integerRegisterFields) + { + return TsType::skip("trivial record returned by value in registers by its fields"); + } + } + + return TsType::tuple(std::move(names), std::move(types)); +} + std::string TypeMapper::referTo(const clang::TagDecl *tag) { - auto key = llvm::isa(tag) ? enumKey(llvm::cast(tag)) - : recordKey(llvm::cast(tag)); + auto *cxxRecord = llvm::dyn_cast(tag); + auto key = llvm::isa(tag) ? enumKey(llvm::cast(tag)) + : cxxRecord && classSkipReason(cxxRecord).empty() ? classKey(cxxRecord->getDefinition()) + : recordKey(llvm::cast(tag)); if (referencedKeys.insert(key).second) { referenced.push_back({key, tag}); @@ -141,8 +329,10 @@ TsType TypeMapper::map(clang::QualType type, Use use) // a system header's typedefs (int32_t, uint8_t, ...) are looked through: their widths are // what matter, and the header is not emitted. So is one named like a tslang type - // (`typedef int boolean;`): tslang would ignore the alias and use its own type. - if (isSystem(typedefDecl) || isTsBuiltinTypeName(typedefDecl->getName().str())) + // (`typedef int boolean;`): tslang would ignore the alias and use its own type. And so is a + // C++ one in a namespace or a class (`nvrhi::MipLevel`), which has no declaration here. + if (isSystem(typedefDecl) || isTsBuiltinTypeName(typedefDecl->getName().str()) || + (context.getLangOpts().CPlusPlus && !typedefDecl->getDeclContext()->getRedeclContext()->isTranslationUnit())) { return map(typedefDecl->getUnderlyingType(), use); } @@ -164,7 +354,13 @@ TsType TypeMapper::map(clang::QualType type, Use use) if (auto *pointer = type->getAs()) { - return mapPointer(pointer, use); + return mapPointer(pointer->getPointeeType(), use); + } + + // a C++ reference is passed as a pointer + if (auto *reference = type->getAs()) + { + return mapPointer(reference->getPointeeType(), use); } if (canonical->isBuiltinType()) @@ -187,9 +383,11 @@ TsType TypeMapper::map(clang::QualType type, Use use) return mapBuiltin(integer.getCanonicalType(), use); } - if (tagName(enumDecl).empty()) + // an anonymous enum's values are plain integers of its underlying type, and so are those of + // a C++ enum in a namespace or a class, which has no declaration of its own here + if (tagName(enumDecl).empty() || + (context.getLangOpts().CPlusPlus && !enumDecl->getDeclContext()->getRedeclContext()->isTranslationUnit())) { - // an anonymous enum's values are plain integers of its underlying type return mapBuiltin(enumDecl->getIntegerType().getCanonicalType(), use); } @@ -278,9 +476,8 @@ TsType TypeMapper::mapBuiltin(clang::QualType canonical, Use use) return TsType::skip("unsupported type '" + canonical.getAsString() + "'"); } -TsType TypeMapper::mapPointer(const clang::PointerType *pointer, Use use) +TsType TypeMapper::mapPointer(clang::QualType pointee, Use use) { - auto pointee = pointer->getPointeeType(); auto canonicalPointee = pointee.getCanonicalType().getUnqualifiedType(); if (isPlainChar(canonicalPointee)) @@ -303,6 +500,12 @@ TsType TypeMapper::mapPointer(const clang::PointerType *pointer, Use use) if (auto *record = canonicalPointee->getAsRecordDecl()) { + // a C++ object of a class bound as a @cpp class: the class, whose values are pointers + if (auto *cxxRecord = llvm::dyn_cast(record); cxxRecord && classSkipReason(cxxRecord).empty()) + { + return TsType::named(referTo(cxxRecord)); + } + // there is no declaration a name could refer to, so no name: the C++ type is a comment if (!isDeclarable(record)) { diff --git a/tslang/test/bindgen/bindgen_cpp_test.ts b/tslang/test/bindgen/bindgen_cpp_test.ts new file mode 100644 index 000000000..8f86e3ac8 --- /dev/null +++ b/tslang/test/bindgen/bindgen_cpp_test.ts @@ -0,0 +1,24 @@ +/// + +// fixture_cpp.hpp's classes, through the @cpp classes tsbindgen generated for this target's ABI + +function main() { + const square = fx_make_square(3); + print(square.sides, square.side); // fields after the vtable pointer, the base's first + print(square.area()); // a virtual method, overridden in C++ + square.scale(2); // scale(int32_t) + print(square.area()); + square.scale_2(0.5); // scale(double), the second overload + print(square.area()); + + const shape: Shape = square; + print(shape.area()); // through the base class + const pair = shape.pair(7); // a trivial record, returned by value + print(pair.a, pair.b); + const handle = shape.handle(); // a non-trivial one (@cppnontrivial) + print(handle.p == shape); + print(shape.id(5)); // a non-virtual method, by its mangled symbol + + const small = fx_make_square(2); + print(square.bigger(small), small.bigger(square)); +} diff --git a/tslang/test/bindgen/expected_cpp.txt b/tslang/test/bindgen/expected_cpp.txt new file mode 100644 index 000000000..ae6687a60 --- /dev/null +++ b/tslang/test/bindgen/expected_cpp.txt @@ -0,0 +1,9 @@ +4 3 +9 +36 +9 +9 +7 3 +true +405 +true false diff --git a/tslang/test/bindgen/fixture_cpp.cpp b/tslang/test/bindgen/fixture_cpp.cpp new file mode 100644 index 000000000..2fee327e3 --- /dev/null +++ b/tslang/test/bindgen/fixture_cpp.cpp @@ -0,0 +1,75 @@ +#include "fixture_cpp.hpp" + +namespace fx +{ + +int32_t Shape::id(int32_t x) +{ + return static_cast(sides * 100 + x); +} + +int32_t Shape::area() const +{ + return 0; +} + +void Shape::scale(int32_t) +{ +} + +void Shape::scale(double) +{ +} + +Handle Shape::handle() +{ + return Handle(nullptr); +} + +Pair Shape::pair(int32_t x) +{ + return Pair{x, 0}; +} + +int32_t Square::area() const +{ + return side * side; +} + +void Square::scale(int32_t by) +{ + side *= by; +} + +void Square::scale(double by) +{ + side = static_cast(side * by); +} + +Handle Square::handle() +{ + return Handle(this); +} + +Pair Square::pair(int32_t x) +{ + return Pair{x, side}; +} + +bool Square::bigger(const Shape &other) const +{ + return area() > other.area(); +} + +} // namespace fx + +// constant-initialized: no constructor runs, so nothing needs the C++ runtime's start-up +static fx::Square squares[2] = {fx::Square(0), fx::Square(0)}; +static int next; + +extern "C" fx::Square *fx_make_square(int32_t side) +{ + auto *square = &squares[next++ % 2]; + square->side = side; + return square; +} diff --git a/tslang/test/bindgen/fixture_cpp.hpp b/tslang/test/bindgen/fixture_cpp.hpp new file mode 100644 index 000000000..08e695b3b --- /dev/null +++ b/tslang/test/bindgen/fixture_cpp.hpp @@ -0,0 +1,86 @@ +// A C++ library for tsbindgen's @cpp classes (run-bindgen-test.cmake, MODE=cpp): its polymorphic +// classes become @cpp classes, called through their vtables and by their mangled symbols. +// +// No C++ runtime is needed to link it: no destructors, pure virtual methods, RTTI or exceptions, and +// its objects are constant-initialized (constexpr constructors), so a tslang program links its object +// alone. + +#ifndef FIXTURE_CPP_HPP +#define FIXTURE_CPP_HPP + +#include + +#ifdef _WIN32 +#define FX_API __declspec(dllexport) +#else +#define FX_API __attribute__((visibility("default"))) +#endif + +namespace fx +{ + +// returned by value: a copy constructor and a destructor make it non-trivial (@cppnontrivial) +struct Handle +{ + void *p; + Handle(void *value) : p(value) + { + } + Handle(const Handle &other) : p(other.p) + { + } + ~Handle() + { + } +}; + +// returned by value: trivial, two 8-byte fields +struct Pair +{ + int64_t a; + int64_t b; +}; + +class FX_API Shape +{ + public: + constexpr Shape(int64_t sides) : sides(sides) + { + } + + // defined (not pure): a pure virtual's slot would point at the C++ runtime's __cxa_pure_virtual + virtual int32_t area() const; + virtual void scale(int32_t by); + virtual void scale(double by); // an overload: scale_2 + virtual Handle handle(); + virtual Pair pair(int32_t x); + + // non-virtual: called by its mangled symbol + int32_t id(int32_t x); + + int64_t sides; // 8 bytes: no tail padding, which the Itanium ABI would put `side` in +}; + +class FX_API Square : public Shape +{ + public: + constexpr Square(int32_t side) : Shape(4), side(side) + { + } + + int32_t area() const override; + void scale(int32_t by) override; + void scale(double by) override; + Handle handle() override; + Pair pair(int32_t x) override; + + virtual bool bigger(const Shape &other) const; + + int32_t side; +}; + +} // namespace fx + +extern "C" FX_API fx::Square *fx_make_square(int32_t side); + +#endif // FIXTURE_CPP_HPP diff --git a/tslang/test/bindgen/run-bindgen-test.cmake b/tslang/test/bindgen/run-bindgen-test.cmake index c00532f14..052a443d9 100644 --- a/tslang/test/bindgen/run-bindgen-test.cmake +++ b/tslang/test/bindgen/run-bindgen-test.cmake @@ -4,7 +4,9 @@ # turns fixture.h into fixture.ts; bindgen_test.ts is compiled against it and linked with the # object (--emit=exe), then run under the JIT against the shared library (--emit=jit). Both must # print expected.txt. MODE=namespace does the same through `--namespace Fx --strip-prefix fx_`, -# which only links because @linkname binds each renamed function to its C symbol. +# which only links because @linkname binds each renamed function to its C symbol. MODE=cpp does it +# for fixture_cpp.hpp's C++ classes, which become @cpp classes: their virtual methods called through +# the vtable the host's clang made, their non-virtual ones by their mangled symbols. cmake_minimum_required(VERSION 3.17.3) @@ -34,10 +36,26 @@ endif() # the C library's stem differs from the bindings' on purpose: `import "./fixture"` next to a # fixture.dll would load that as a tslang library +set(header "fixture.h") +set(source "fixture.c") +set(expected_file "expected.txt") +set(clang_language "") +set(tslang_mm "") if(MODE STREQUAL "namespace") set(bindings "fixture_ns.ts") set(program "bindgen_ns_test.ts") set(bindgen_args --namespace Fx --strip-prefix fx_) +elseif(MODE STREQUAL "cpp") + set(header "fixture_cpp.hpp") + set(source "fixture_cpp.cpp") + set(expected_file "expected_cpp.txt") + set(bindings "fixture_cpp.ts") + set(program "bindgen_cpp_test.ts") + set(bindgen_args "") + # no destructors, RTTI or exceptions in fixture_cpp: nothing of the C++ runtime is linked + set(clang_language -fno-rtti -fno-exceptions) + # a @cpp class needs the gc or none memory model + set(tslang_mm -mm=gc) else() set(bindings "fixture.ts") set(program "bindgen_test.ts") @@ -46,11 +64,11 @@ endif() file(REMOVE_RECURSE "${WORK_DIR}") file(MAKE_DIRECTORY "${WORK_DIR}") -file(COPY "${SOURCE_DIR}/fixture.h" "${SOURCE_DIR}/fixture.c" "${SOURCE_DIR}/${program}" DESTINATION "${WORK_DIR}") -file(READ "${SOURCE_DIR}/expected.txt" expected) +file(COPY "${SOURCE_DIR}/${header}" "${SOURCE_DIR}/${source}" "${SOURCE_DIR}/${program}" DESTINATION "${WORK_DIR}") +file(READ "${SOURCE_DIR}/${expected_file}" expected) string(REPLACE "\r\n" "\n" expected "${expected}") -set(common --no-default-lib ${OPT} ${pic} "--gc-lib-path=${GC_LIB}" "--llvm-lib-path=${LLVM_LIB}" +set(common --no-default-lib ${OPT} ${pic} ${tslang_mm} "--gc-lib-path=${GC_LIB}" "--llvm-lib-path=${LLVM_LIB}" "--tslang-lib-path=${TSLANG_LIB}") # run( ) - fails unless it exits 0; leaves stdout in run_output @@ -76,9 +94,9 @@ endfunction() # -O2 as a real library would be built: at -O0 clang re-extends a narrow parameter inside the callee, # which would hide a caller that does not extend it (the `narrow` line, on the SysV ABI) -run("clang -c fixture.c" "${CLANG}" -O2 -c ${clang_pic} fixture.c -o "fixture${obj_suffix}") -run("clang -shared fixture.c" "${CLANG}" -O2 -shared ${clang_pic} fixture.c -o "${fixture_library}") -run("tsbindgen" "${TSBINDGEN}" fixture.h ${bindgen_args} -o "${bindings}") +run("clang -c ${source}" "${CLANG}" -O2 -c ${clang_pic} ${clang_language} "${source}" -o "fixture${obj_suffix}") +run("clang -shared ${source}" "${CLANG}" -O2 -shared ${clang_pic} ${clang_language} "${source}" -o "${fixture_library}") +run("tsbindgen" "${TSBINDGEN}" "${header}" ${bindgen_args} -o "${bindings}") run("--emit=exe" "${TSLANG}" --emit=exe ${common} "${program}" "--obj=fixture${obj_suffix}" -o "bindgen_test${exe_suffix}") run("the program" "${WORK_DIR}/bindgen_test${exe_suffix}") diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index f39ea3c92..891d8b56e 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -2609,8 +2609,9 @@ endif() # tsbindgen: bindings generated from test/bindgen/fixture.h call the C library compiled from # fixture.c, linked (--emit=exe) and under the JIT; `namespace` does it again through -# --namespace/--strip-prefix, which @linkname binds. And the command line's exit codes. -foreach(bindgen_mode plain namespace) +# --namespace/--strip-prefix, which @linkname binds; `cpp` calls the C++ classes of +# fixture_cpp.hpp through the @cpp classes generated for them. And the command line's exit codes. +foreach(bindgen_mode plain namespace cpp) add_test(NAME test-bindgen-${bindgen_mode} COMMAND ${CMAKE_COMMAND} "-DMODE=${bindgen_mode}" diff --git a/tslang/unittests/TsClangImporter/CMakeLists.txt b/tslang/unittests/TsClangImporter/CMakeLists.txt index 9c9e76d7f..39f754955 100644 --- a/tslang/unittests/TsClangImporter/CMakeLists.txt +++ b/tslang/unittests/TsClangImporter/CMakeLists.txt @@ -5,6 +5,7 @@ add_mlir_unittest(TsClangImporterTests SelectionTest.cpp NamingTest.cpp ParseErrorTest.cpp + CppClassTest.cpp ) target_link_libraries(TsClangImporterTests diff --git a/tslang/unittests/TsClangImporter/CppClassTest.cpp b/tslang/unittests/TsClangImporter/CppClassTest.cpp new file mode 100644 index 000000000..e2f2b4e47 --- /dev/null +++ b/tslang/unittests/TsClangImporter/CppClassTest.cpp @@ -0,0 +1,175 @@ +// A polymorphic C++ class is a @cpp class: its virtual methods @slot(N), with the slot clang's +// vtable layout gives for the target's ABI, its non-virtual ones @linkname with their mangled +// symbols. The slot numbers below are the compilers' own: MSVC's from +// `cl /d1reportSingleClassLayoutCircle`, the Itanium ABI's from clang's -fdump-vtable-layouts. + +#include "ImporterTestHelper.h" + +#include "gmock/gmock.h" + +using namespace tsbindgen_test; +using testing::AllOf; +using testing::Contains; +using testing::HasSubstr; +using testing::Not; + +namespace +{ + +const char *const shapes = "namespace geo {\n" + "class Shape {\n" + "public:\n" + " virtual ~Shape();\n" + " virtual int area() const = 0;\n" + " virtual void scale(int by) = 0;\n" + " virtual void scale(float by) = 0;\n" + " virtual void scale(double by) = 0;\n" + "protected:\n" + " virtual void hidden() = 0;\n" + "public:\n" + " virtual int last() = 0;\n" + "};\n" + "class Circle : public Shape {\n" + "public:\n" + " int area() const override;\n" + " virtual bool contains(const Shape &other) const;\n" + " void scale(float by) override;\n" + "};\n" + "}\n"; + +std::string cpp(const std::string &header, const std::string &target = windowsTarget) +{ + return generate(header, {}, target, {}, "c++").text; +} + +} // namespace + +// MSVC: one destructor slot, and overloads grouped in reverse declaration order +TEST(CppClass, MsvcSlots) +{ + auto out = cpp(shapes); + EXPECT_THAT(out, HasSubstr("@cpp\nclass Shape { // geo::Shape\n" + " @slot(1) area(): s32;\n" + " @slot(4) scale(by: s32): void;\n" + " @slot(3) scale_2(by: f32): void;\n" + " @slot(2) scale_3(by: f64): void;\n" + " @slot(6) last(): s32;\n" + "}\n")); + // the destructor (0) and the protected hidden() (5) are slots left out; overrides aren't + // declared again: a @cpp class's override keeps its base's slot by name + EXPECT_THAT(out, HasSubstr("@cpp\nclass Circle extends Shape { // geo::Circle\n" + " @slot(7) contains(other: Shape): boolean;\n" + "}\n")); +} + +// Itanium: two destructor slots, overloads in declaration order +TEST(CppClass, ItaniumSlots) +{ + auto out = cpp(shapes, linuxTarget); + EXPECT_THAT(out, HasSubstr(" @slot(2) area(): s32;\n" + " @slot(3) scale(by: s32): void;\n" + " @slot(4) scale_2(by: f32): void;\n" + " @slot(5) scale_3(by: f64): void;\n" + " @slot(7) last(): s32;\n")); + EXPECT_THAT(out, HasSubstr(" @slot(8) contains(other: Shape): boolean;\n")); +} + +TEST(CppClass, NonVirtualMethodsAreBoundByTheirMangledSymbols) +{ + auto header = "namespace geo { class Shape { public: virtual ~Shape(); int id(int x); int inlined() { return 1; }\n" + "static int count(); int operator+(int x); }; }\n"; + auto windows = generate(header, {}, windowsTarget, {}, "c++"); + EXPECT_THAT(windows.text, HasSubstr(" @linkname(\"?id@Shape@geo@@QEAAHH@Z\") id(x: s32): s32;\n")); + EXPECT_THAT(windows.text, HasSubstr(" // skipped: inlined \xE2\x80\x94 inline method: no symbol to link against\n")); + EXPECT_THAT(windows.warnings, Contains("skipped geo::Shape::inlined: inline method: no symbol to link against")); + // static members and operators aren't methods a @cpp class can have + EXPECT_THAT(windows.text, AllOf(Not(HasSubstr("count")), Not(HasSubstr("operator")))); + + EXPECT_THAT(cpp(header, linuxTarget), HasSubstr(" @linkname(\"_ZN3geo5Shape2idEi\") id(x: s32): s32;\n")); +} + +// A record returned by value is a tuple with its layout; one C++ can't pass in registers (a +// destructor, a copy constructor) is @cppnontrivial, which decides where the Itanium ABI puts it +TEST(CppClass, RecordsReturnedByValue) +{ + auto header = "struct Ptr { void *p; Ptr(const Ptr &o); ~Ptr(); };\n" + "struct Obj { void *p; };\n" + "struct Two { unsigned long long a; void *b; };\n" + "struct Three { int a; int b; int c; };\n" + "class Maker { public: virtual ~Maker(); virtual Ptr make(int x) = 0; virtual Obj obj() = 0;\n" + "virtual Two two() = 0; virtual Three three() = 0; Obj plain(); };\n"; + auto windows = cpp(header); + EXPECT_THAT(windows, HasSubstr(" @cppnontrivial\n @slot(1) make(x: s32): [p: Opaque | null];\n")); + EXPECT_THAT(windows, HasSubstr(" @slot(2) obj(): [p: Opaque | null];\n")); + EXPECT_THAT(windows, HasSubstr(" @slot(3) two(): [a: u64, b: Opaque | null];\n")); + // MSVC returns any record through a hidden pointer + EXPECT_THAT(windows, HasSubstr(" @slot(4) three(): [a: s32, b: s32, c: s32];\n")); + EXPECT_THAT(windows, HasSubstr("// skipped: plain \xE2\x80\x94 a non-virtual method returning a record by value " + "isn't supported yet")); + + auto linux = cpp(header, linuxTarget); + EXPECT_THAT(linux, HasSubstr(" @cppnontrivial\n @slot(2) make(x: s32): [p: Opaque | null];\n")); + EXPECT_THAT(linux, HasSubstr(" @slot(4) two(): [a: u64, b: Opaque | null];\n")); + // the Itanium ABI returns a trivial record in registers by its fields, which tslang does for + // one or two 8-byte integer or pointer fields only + EXPECT_THAT(linux, HasSubstr("// skipped: three \xE2\x80\x94 result: trivial record returned by value in " + "registers by its fields")); +} + +// A pointer or a reference to a @cpp class's object is the class; a C++ reference to anything +// else is a pointer; an enum or a typedef of a namespace is its integer type +TEST(CppClass, ParameterTypes) +{ + auto out = cpp("struct Point { int x; int y; };\n" + "namespace geo { enum class Kind : unsigned char { Circle, Square }; typedef unsigned int Layer;\n" + "class Canvas { public: virtual ~Canvas(); virtual void draw(Canvas &other, const Canvas *again,\n" + "const Point &at, Kind kind, bool fill, Layer layer) = 0; }; }\n" + "extern \"C\" geo::Canvas *make_canvas(int width);\n"); + EXPECT_THAT(out, HasSubstr("type Point = [x: s32, y: s32];")); + EXPECT_THAT(out, HasSubstr(" @slot(1) draw(other: Canvas, again: Canvas, at: Reference, kind: u8, " + "fill: boolean, layer: u32): void;\n")); + EXPECT_THAT(out, Not(HasSubstr("Layer"))); + EXPECT_THAT(out, HasSubstr("declare function make_canvas(width: s32): Canvas;")); +} + +// Fields follow the vtable pointer and the base's fields, as a @cpp class lays them out +TEST(CppClass, Fields) +{ + auto out = cpp("class Base { public: virtual ~Base(); int a; };\n" + "class Derived : public Base { public: virtual void f(); double b; };\n"); + EXPECT_THAT(out, HasSubstr("class Base { // Base\n a: s32;\n}\n")); + EXPECT_THAT(out, HasSubstr("class Derived extends Base { // Derived\n b: f64;\n @slot(1) f(): void;\n}\n")); + + // a @cpp class lays a derived class's fields out after its base's storage, padded to its + // alignment, as MSVC does; the Itanium ABI puts them in the base's tail padding, so there they + // aren't declared + auto padded = "class Base { public: virtual ~Base(); int a; };\n" + "class Derived : public Base { public: int b; };\n"; + EXPECT_THAT(cpp(padded), HasSubstr("class Derived extends Base { // Derived\n b: s32;\n}\n")); + EXPECT_THAT(cpp(padded, linuxTarget), + HasSubstr("class Derived extends Base { // Derived\n" + " // fields not declared: field 'b' isn't where a tuple would put it\n}\n")); +} + +// What a @cpp class can't be stays Opaque, as before +TEST(CppClass, ClassesThatArentBound) +{ + auto out = cpp("class A { public: virtual ~A(); };\nclass B { public: virtual ~B(); };\n" + "class Both : public A, public B { public: virtual void f(); };\n" + "class Virtual : public virtual A { public: virtual void g(); };\n" + "template class Box { public: virtual T get(); };\n" + "extern \"C\" void use(Both *both, Virtual *v, Box *box);\n"); + EXPECT_THAT(out, HasSubstr("// skipped: Both \xE2\x80\x94 C++ class")); + EXPECT_THAT(out, HasSubstr("// skipped: Virtual \xE2\x80\x94 C++ class")); + EXPECT_THAT(out, HasSubstr("declare function use(both: Both, v: Virtual, box: Opaque /* Box */): void;")); + EXPECT_THAT(out, AllOf(HasSubstr("class A "), HasSubstr("class B "))); +} + +// --filter picks a class by its name or its qualified name, and brings its base along +TEST(CppClass, FilterByQualifiedName) +{ + tsbindgen::PrintOptions options; + options.filters = {"geo::Circle"}; + auto out = generate(shapes, options, windowsTarget, {}, "c++").text; + EXPECT_THAT(out, AllOf(HasSubstr("class Shape "), HasSubstr("class Circle extends Shape "))); +} diff --git a/tslang/unittests/TsClangImporter/TypeMappingTest.cpp b/tslang/unittests/TsClangImporter/TypeMappingTest.cpp index 64881812a..6a0a41d9d 100644 --- a/tslang/unittests/TsClangImporter/TypeMappingTest.cpp +++ b/tslang/unittests/TsClangImporter/TypeMappingTest.cpp @@ -61,9 +61,10 @@ TEST(TypeMapping, VoidPointerAndIncompleteStructAreOpaque) EXPECT_THAT(out, HasSubstr("declare function f(a: Opaque, c: Counter, o: Other): Opaque;")); } +// (a polymorphic class is a @cpp class: CppClassTest.cpp) TEST(TypeMapping, PointerToCxxClassIsOpaque) { - auto out = generate("class Engine { public: virtual ~Engine(); int power; };\n" + auto out = generate("class Engine { public: Engine(); int power; };\n" "extern \"C\" int engine_power(Engine *e);\n", {}, windowsTarget, {}, "c++") .text; @@ -76,7 +77,7 @@ TEST(TypeMapping, PointerToCxxClassIsOpaque) // Opaque, with the C++ type in a comment - never a name the output does not declare. TEST(TypeMapping, PointerToANamespacedOrTemplateClassIsPlainOpaque) { - auto out = generate("namespace ns { class Engine { public: virtual ~Engine(); }; }\n" + auto out = generate("namespace ns { class Engine { public: Engine(); }; }\n" "template struct Box { T value; };\n" "typedef Box CharBox;\n" "extern \"C\" int use(ns::Engine *e, Box *b, CharBox *c);\n",