diff --git a/README.md b/README.md index 6aaf45d25..8c1727368 100644 --- a/README.md +++ b/README.md @@ -591,9 +591,11 @@ const MAX_ITEMS = 16; type Point = [x: s32, y: s32]; declare function add(a: s32, b: s32): s32; // skipped: helper — static function: no symbol to link against -declare function dist(p: Reference): f64; +declare function dist(p: Reference | null): f64; ``` +A pointer parameter or struct field is nullable (`| null`), as C lets it be null; a C++ reference (`const Point &`) and a result aren't. + Include the result with `/// `, not `import`. Name it `.ts`, not `.d.ts`: a `.d.ts` file is read as declarations only, so its constants would have no value. ### When nothing is emitted @@ -605,8 +607,8 @@ Include the result with `/// `, not `import`. Name ``` ```typescript - declare function puts(_Buffer: string): s32; - @varargs declare function printf(_Format: string): s32; + declare function puts(_Buffer: string | null): s32; + @varargs declare function printf(_Format: string | null): s32; ``` - **`static` and `static inline` functions are skipped**: there is no symbol to link against. @@ -665,13 +667,15 @@ int cxx_only(int a); it writes: ```typescript +// skipped: Widget — empty struct +type Widget = Opaque; type Point = [x: s32, y: s32]; enum Mode { A = 1, B = 2 } declare function add(a: s32, b: s32): s32; -declare function dist(p: Reference): f64; +declare function dist(p: Reference | null): f64; ``` -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. +Structs, enums and `extern "C"` functions are emitted, and polymorphic classes (below). A struct or a class without virtual methods, in any namespace or class, is a tuple when its fields are where a tuple's are: constructors, methods and member initializers don't move them, so `struct Color { float r, g, b, a; Color(); }` is `type Color = [r: f32, g: f32, b: f32, a: f32];`. It is declared under its own name, and `--filter` matches that or the qualified name (`--filter 'nvrhi::*'`); of two of one name in different namespaces, the next is named with its namespaces (`rt_Desc` for `nvrhi::rt::Desc` under `--namespace nvrhi`). One that isn't a tuple is `type Name = Opaque;`, with the reason in a `// skipped:` line: a derived class, or a field tsbindgen can't spell (a template's specialization such as `std::string` or `std::vector`, an array, a bitfield, a union). 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 @@ -697,6 +701,7 @@ extern "C" geo::Square *make_square(int side); `tsbindgen shapes.hpp --target x86_64-pc-windows-msvc` writes: ```typescript +type Handle = [p: Opaque | null]; @cpp class Shape { // geo::Shape @slot(1) area(): s32; @@ -716,7 +721,7 @@ 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 pointer or a reference** to an object of a bound class is the class (`| null` for a pointer); a reference to a struct is a `Reference` to its tuple. - **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. diff --git a/tslang/include/TsClangImporter/BindingModel.h b/tslang/include/TsClangImporter/BindingModel.h index a0ccbbeea..25a61bd14 100644 --- a/tslang/include/TsClangImporter/BindingModel.h +++ b/tslang/include/TsClangImporter/BindingModel.h @@ -26,6 +26,7 @@ struct TsType std::vector fieldNames; // Tuple std::string comment; // shown next to the type: a function pointer's C signature, ... bool functionPointer = false; + bool nullable = false; // spelled `T | null`: a pointer parameter or field, which C lets be null std::string skipReason; // non-empty: the C type has no tslang mapping bool skipped() const @@ -46,7 +47,7 @@ std::string renderType(const TsType &type, const std::function &keys); -// "struct:Point" -> "Point" +// "struct:Point" -> "Point", "class:nvrhi::IDevice" -> "IDevice" std::string nameFromKey(const std::string &key); // A name that is a TS reserved word gets a `_` suffix: `delete` -> `delete_`. @@ -110,7 +111,7 @@ struct Decl 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 qualifiedName; // CppClass, a C++ Struct: 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 diff --git a/tslang/include/TsClangImporter/TypeMapper.h b/tslang/include/TsClangImporter/TypeMapper.h index 19f83533b..51224604e 100644 --- a/tslang/include/TsClangImporter/TypeMapper.h +++ b/tslang/include/TsClangImporter/TypeMapper.h @@ -36,6 +36,7 @@ class TypeMapper { } + // A pointer parameter or field is nullable (`T | null`); a pointer result isn't. TsType map(clang::QualType type, Use use); // Why a complete struct cannot be a named tuple ("" if it can). Cached; a struct that refers to @@ -48,6 +49,8 @@ class TypeMapper // The struct/enum's own name, or the typedef that names it when it has none ("" for neither). static std::string tagName(const clang::TagDecl *tag); + // tagName with its namespaces and classes for a C++ struct out of the file scope (`nvrhi::Color`) + static std::string qualifiedTagName(const clang::TagDecl *tag); static std::string recordKey(const clang::RecordDecl *record); static std::string enumKey(const clang::EnumDecl *enumDecl); static std::string typedefKey(const clang::TypedefNameDecl *typedefDecl); @@ -59,8 +62,9 @@ class TypeMapper // than int; anything else is a type alias plus constants. bool isPlainEnum(const clang::EnumDecl *enumDecl); - // Whether a struct can have a TS declaration of its own: in C, any; in C++, not one inside a - // namespace or a class, and not a template or a template specialization (std::cmatch). + // Whether a struct can have a TS declaration of its own: in C, any; in C++, one in a namespace or + // a class too (declared under its own name), but not a template, a template specialization + // (std::cmatch) or a class inside one, nor a class local to a function. 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 @@ -91,6 +95,7 @@ class TypeMapper } private: + TsType mapType(clang::QualType type, Use use); std::string referTo(const clang::TagDecl *tag); TsType mapPointer(clang::QualType pointee, Use use); TsType mapBuiltin(clang::QualType canonical, Use use); diff --git a/tslang/lib/TsClangImporter/BindingModel.cpp b/tslang/lib/TsClangImporter/BindingModel.cpp index dc663707f..b7a9d89b7 100644 --- a/tslang/lib/TsClangImporter/BindingModel.cpp +++ b/tslang/lib/TsClangImporter/BindingModel.cpp @@ -48,6 +48,13 @@ TsType TsType::skip(std::string reason) std::string renderType(const TsType &type, const std::function &nameOf) { + if (type.nullable) + { + auto nonNull = type; + nonNull.nullable = false; + return renderType(nonNull, nameOf) + " | null"; + } + switch (type.kind) { case TsType::Kind::Builtin: @@ -86,7 +93,9 @@ void collectKeys(const TsType &type, std::vector &keys) std::string nameFromKey(const std::string &key) { auto colon = key.find(':'); - return colon == std::string::npos ? key : key.substr(colon + 1); + auto name = colon == std::string::npos ? key : key.substr(colon + 1); + auto scope = name.rfind("::"); + return scope == std::string::npos ? name : name.substr(scope + 2); } std::string escapeReserved(std::string name) diff --git a/tslang/lib/TsClangImporter/BindingPrinter.cpp b/tslang/lib/TsClangImporter/BindingPrinter.cpp index 129ce11ad..c3acead49 100644 --- a/tslang/lib/TsClangImporter/BindingPrinter.cpp +++ b/tslang/lib/TsClangImporter/BindingPrinter.cpp @@ -139,6 +139,7 @@ llvm::Expected printBindings(const HeaderModel &model, const PrintO // name another declaration already has llvm::StringMap tsNames; llvm::StringMap taken; + llvm::StringMap takenTypes; // a type's TS name -> its C++ name for (size_t index = 0; index < model.decls.size(); ++index) { if (!selected[index]) @@ -172,7 +173,45 @@ llvm::Expected printBindings(const HeaderModel &model, const PrintO name += "_"; } + // two C++ types of one name in different namespaces (nvrhi::ShaderDesc, nvrhi::rt::...): the + // first keeps it, the next is named with its namespaces (`rt_ShaderDesc` for `nvrhi::rt::` + // under --namespace nvrhi) + if (decl.kind != DeclKind::Function && decl.kind != DeclKind::Macro && !decl.qualifiedName.empty() && + takenTypes.count(name)) + { + llvm::StringRef qualified(decl.qualifiedName); + if (!options.namespaceName.empty()) + { + qualified.consume_front(options.namespaceName + "::"); + } + + std::string unique; + for (auto c : qualified) + { + unique += std::isalnum(static_cast(c)) ? c : '_'; + } + + while (unique.find("__") != std::string::npos) + { + unique.replace(unique.find("__"), 2, "_"); + } + + while (takenTypes.count(unique) || taken.count(unique)) + { + unique += "_"; + } + + result.warnings.push_back("named " + decl.qualifiedName + " '" + unique + "': '" + name + "' is taken by " + + takenTypes[name]); + name = unique; + } + taken[name] = decl.name; + if (decl.kind != DeclKind::Function && decl.kind != DeclKind::Macro) + { + takenTypes[name] = decl.qualifiedName.empty() ? decl.name : decl.qualifiedName; + } + tsNames[decl.key] = name; } diff --git a/tslang/lib/TsClangImporter/HeaderParser.cpp b/tslang/lib/TsClangImporter/HeaderParser.cpp index 969d928e9..be2ea0397 100644 --- a/tslang/lib/TsClangImporter/HeaderParser.cpp +++ b/tslang/lib/TsClangImporter/HeaderParser.cpp @@ -265,9 +265,16 @@ class Collector : public clang::RecursiveASTVisitor return true; } - // completed below, once the whole file is seen: its definition may come later + // completed below, once the whole file is seen: its definition may come later. A C++ struct + // in a namespace or a class is declared under its own name; --filter matches either. pendingRecords.push_back({model.decls.size(), record}); - add(DeclKind::OpaqueStruct, key, TypeMapper::tagName(record), record); + auto &decl = add(DeclKind::OpaqueStruct, key, TypeMapper::tagName(record), record); + auto qualified = TypeMapper::qualifiedTagName(record); + if (qualified != decl.name) + { + decl.qualifiedName = qualified; + } + return true; } diff --git a/tslang/lib/TsClangImporter/TypeMapper.cpp b/tslang/lib/TsClangImporter/TypeMapper.cpp index 959052340..5c4187623 100644 --- a/tslang/lib/TsClangImporter/TypeMapper.cpp +++ b/tslang/lib/TsClangImporter/TypeMapper.cpp @@ -44,9 +44,29 @@ std::string TypeMapper::tagName(const clang::TagDecl *tag) return ""; } +std::string TypeMapper::qualifiedTagName(const clang::TagDecl *tag) +{ + if (!llvm::isa(tag) || tag->getDeclContext()->getRedeclContext()->isTranslationUnit()) + { + return tagName(tag); + } + + if (!tag->getName().empty()) + { + return tag->getQualifiedNameAsString(); + } + + if (auto *typedefDecl = tag->getTypedefNameForAnonDecl()) + { + return typedefDecl->getQualifiedNameAsString(); + } + + return ""; +} + std::string TypeMapper::recordKey(const clang::RecordDecl *record) { - return "struct:" + tagName(record); + return "struct:" + qualifiedTagName(record); } std::string TypeMapper::enumKey(const clang::EnumDecl *enumDecl) @@ -293,14 +313,18 @@ bool TypeMapper::isDeclarable(const clang::RecordDecl *record) return true; // in C, a struct declared inside another is still file-scoped } - if (!record->getDeclContext()->getRedeclContext()->isTranslationUnit()) + // a template, a specialization or a class inside one (std::vector's members), and a class + // inside a function or a lambda's, have no name the bindings could declare + if (record->isDependentContext() || record->isInvalidDecl() || record->getParentFunctionOrMethod()) { return false; } auto *cxxRecord = llvm::dyn_cast(record); + // (a specialization only named, never instantiated, is TSK_Undeclared, hence the isa) return !cxxRecord || - (!cxxRecord->getDescribedClassTemplate() && !llvm::isa(cxxRecord)); + (!cxxRecord->getDescribedClassTemplate() && !llvm::isa(cxxRecord) && + cxxRecord->getTemplateSpecializationKind() == clang::TSK_Undeclared && !cxxRecord->isLambda()); } bool TypeMapper::isSystem(const clang::Decl *decl) @@ -310,6 +334,20 @@ bool TypeMapper::isSystem(const clang::Decl *decl) } TsType TypeMapper::map(clang::QualType type, Use use) +{ + auto mapped = mapType(type, use); + // C lets a pointer argument or field be null (an optional out-parameter, a missing callback, a + // desc's unused buffer); a C++ reference can't be. A result isn't marked, which would make every + // call site check it. + if ((use == Use::Parameter || use == Use::Field) && !mapped.skipped() && type.getCanonicalType()->isPointerType()) + { + mapped.nullable = true; + } + + return mapped; +} + +TsType TypeMapper::mapType(clang::QualType type, Use use) { type = type.getUnqualifiedType(); @@ -579,9 +617,19 @@ std::string TypeMapper::recordSkipReason(const clang::RecordDecl *record) return "union"; } + // a C++ struct is a tuple when its fields are where a tuple's are: methods, constructors and + // member initializers don't move them, a vtable pointer or a base's storage does if (auto *cxxRecord = llvm::dyn_cast(record); cxxRecord && !cxxRecord->isCLike()) { - return "C++ class"; + if (cxxRecord->isPolymorphic()) + { + return "C++ class with virtual methods"; + } + + if (cxxRecord->getNumBases() > 0) + { + return "derived C++ class"; + } } if (record->field_empty()) diff --git a/tslang/test/bindgen/bindgen_cpp_test.ts b/tslang/test/bindgen/bindgen_cpp_test.ts index 8f86e3ac8..88904847d 100644 --- a/tslang/test/bindgen/bindgen_cpp_test.ts +++ b/tslang/test/bindgen/bindgen_cpp_test.ts @@ -18,6 +18,8 @@ function main() { 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 + let extent: Extent = [3, 4]; // fx::Extent, a struct in a namespace: a tuple + print(shape.fits(Ref(extent), null), shape.fits(Ref(extent), Ref(extent))); // a reference, a pointer or null 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 index ae6687a60..d89bfc013 100644 --- a/tslang/test/bindgen/expected_cpp.txt +++ b/tslang/test/bindgen/expected_cpp.txt @@ -6,4 +6,5 @@ 7 3 true 405 +-988 3012 true false diff --git a/tslang/test/bindgen/fixture_cpp.cpp b/tslang/test/bindgen/fixture_cpp.cpp index 2fee327e3..fcd50273f 100644 --- a/tslang/test/bindgen/fixture_cpp.cpp +++ b/tslang/test/bindgen/fixture_cpp.cpp @@ -31,6 +31,11 @@ Pair Shape::pair(int32_t x) return Pair{x, 0}; } +int32_t Shape::fits(const Extent &extent, const Extent *limit) +{ + return (limit ? limit->width : -1) * 1000 + extent.width * extent.height; +} + int32_t Square::area() const { return side * side; diff --git a/tslang/test/bindgen/fixture_cpp.hpp b/tslang/test/bindgen/fixture_cpp.hpp index 08e695b3b..5732b99bb 100644 --- a/tslang/test/bindgen/fixture_cpp.hpp +++ b/tslang/test/bindgen/fixture_cpp.hpp @@ -41,6 +41,17 @@ struct Pair int64_t b; }; +// a struct in a namespace, with a constructor: still a tuple, passed by reference or by a pointer +// that may be null +struct Extent +{ + int32_t width; + int32_t height; + constexpr Extent(int32_t width, int32_t height) : width(width), height(height) + { + } +}; + class FX_API Shape { public: @@ -54,6 +65,7 @@ class FX_API Shape virtual void scale(double by); // an overload: scale_2 virtual Handle handle(); virtual Pair pair(int32_t x); + virtual int32_t fits(const Extent &extent, const Extent *limit); // non-virtual: called by its mangled symbol int32_t id(int32_t x); diff --git a/tslang/unittests/TsClangImporter/CMakeLists.txt b/tslang/unittests/TsClangImporter/CMakeLists.txt index 39f754955..28a825f2b 100644 --- a/tslang/unittests/TsClangImporter/CMakeLists.txt +++ b/tslang/unittests/TsClangImporter/CMakeLists.txt @@ -6,6 +6,7 @@ add_mlir_unittest(TsClangImporterTests NamingTest.cpp ParseErrorTest.cpp CppClassTest.cpp + NamespacedStructTest.cpp ) target_link_libraries(TsClangImporterTests diff --git a/tslang/unittests/TsClangImporter/CppClassTest.cpp b/tslang/unittests/TsClangImporter/CppClassTest.cpp index e2f2b4e47..7d85959ae 100644 --- a/tslang/unittests/TsClangImporter/CppClassTest.cpp +++ b/tslang/unittests/TsClangImporter/CppClassTest.cpp @@ -126,7 +126,7 @@ TEST(CppClass, ParameterTypes) "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, " + EXPECT_THAT(out, HasSubstr(" @slot(1) draw(other: Canvas, again: Canvas | null, 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;")); @@ -161,7 +161,7 @@ TEST(CppClass, ClassesThatArentBound) "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, HasSubstr("declare function use(both: Both | null, v: Virtual | null, box: Opaque | null /* Box */): void;")); EXPECT_THAT(out, AllOf(HasSubstr("class A "), HasSubstr("class B "))); } diff --git a/tslang/unittests/TsClangImporter/NamespacedStructTest.cpp b/tslang/unittests/TsClangImporter/NamespacedStructTest.cpp new file mode 100644 index 000000000..362f28380 --- /dev/null +++ b/tslang/unittests/TsClangImporter/NamespacedStructTest.cpp @@ -0,0 +1,112 @@ +// A C++ struct in a namespace or a class is declared under its own name, a tuple when its fields +// are where a tuple's are (constructors, methods and member initializers don't move them), and a +// pointer parameter to it is nullable where a C++ reference isn't. + +#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 gfx = "namespace gfx {\n" + "struct Color {\n" + " float r, g, b, a;\n" + " Color() : r(0), g(0), b(0), a(0) {}\n" + " Color(float value);\n" + " bool operator==(const Color &other) const;\n" + "};\n" + "struct Desc { unsigned width = 1; bool cpuAccess = false; Color clear; };\n" + "class Device {\n" + "public:\n" + " virtual ~Device();\n" + " virtual void clear(const Color &color, Desc *desc) = 0;\n" + "};\n" + "}\n" + "extern \"C\" gfx::Device *make_device();\n"; + +std::string cpp(const std::string &header, tsbindgen::PrintOptions options = {}) +{ + return generate(header, std::move(options), windowsTarget, {}, "c++").text; +} + +} // namespace + +TEST(NamespacedStruct, IsATuple) +{ + auto out = cpp(gfx); + EXPECT_THAT(out, HasSubstr("type Color = [r: f32, g: f32, b: f32, a: f32];")); + EXPECT_THAT(out, HasSubstr("type Desc = [width: u32, cpuAccess: boolean, clear: Color];")); + EXPECT_THAT(out, HasSubstr(" @slot(1) clear(color: Reference, desc: Reference | null): void;\n")); +} + +// --filter picks a struct by its name or its qualified name, and what it uses comes along +TEST(NamespacedStruct, FilterByNameOrQualifiedName) +{ + tsbindgen::PrintOptions qualified; + qualified.filters = {"gfx::Desc"}; + auto out = cpp(gfx, qualified); + EXPECT_THAT(out, AllOf(HasSubstr("type Desc = "), HasSubstr("type Color = "))); + EXPECT_THAT(out, Not(HasSubstr("class Device"))); + + tsbindgen::PrintOptions plain; + plain.filters = {"Color"}; + EXPECT_THAT(cpp(gfx, plain), HasSubstr("type Color = ")); +} + +// The first of two structs of one name keeps it; the next is named with its namespaces, less the +// --namespace the bindings are wrapped in +TEST(NamespacedStruct, SameNameInTwoNamespaces) +{ + tsbindgen::PrintOptions options; + options.namespaceName = "outer"; + auto generated = generate("namespace outer {\n" + "struct Desc { int x; };\n" + "namespace rt { struct Desc { double y; }; }\n" + "}\n" + "extern \"C\" void use(outer::Desc *a, outer::rt::Desc *b);\n", + options, windowsTarget, {}, "c++"); + EXPECT_THAT(generated.text, HasSubstr(" export type Desc = [x: s32];")); + EXPECT_THAT(generated.text, HasSubstr(" export type rt_Desc = [y: f64];")); + EXPECT_THAT(generated.text, HasSubstr("use(a: Reference | null, b: Reference | null): void;")); + EXPECT_THAT(generated.warnings, Contains("named outer::rt::Desc 'rt_Desc': 'Desc' is taken by outer::Desc")); +} + +TEST(NamespacedStruct, NestedInAClass) +{ + auto out = cpp("namespace n { struct Outer { struct Inner { int v; }; Inner inner; int w; }; }\n" + "extern \"C\" int use(n::Outer::Inner *i, const n::Outer *o);\n"); + EXPECT_THAT(out, HasSubstr("type Inner = [v: s32];")); + EXPECT_THAT(out, HasSubstr("type Outer = [inner: Inner, w: s32];")); + EXPECT_THAT(out, HasSubstr("declare function use(i: Reference | null, o: Reference | null): s32;")); +} + +// A derived struct, and one holding a template's specialization (std::string, a container), are +// named but Opaque: their layouts aren't a tuple's, or not one tsbindgen can spell +TEST(NamespacedStruct, WhatStaysOpaque) +{ + auto out = cpp("template struct Vec { T *data; };\n" + "namespace n {\n" + "struct Base { int a; };\n" + "struct Derived : Base { int b; };\n" + "struct Holder { Vec values; };\n" + "}\n" + "extern \"C\" void use(n::Derived *d, n::Holder *h);\n"); + EXPECT_THAT(out, HasSubstr("// skipped: Derived \xE2\x80\x94 derived C++ class\ntype Derived = Opaque;")); + EXPECT_THAT(out, HasSubstr("// skipped: Holder \xE2\x80\x94 field 'values': C++ class 'struct Vec'\ntype Holder = Opaque;")); + EXPECT_THAT(out, HasSubstr("declare function use(d: Derived | null, h: Holder | null): void;")); + EXPECT_THAT(out, Not(HasSubstr("type Vec"))); +} + +// A struct passed by value is still left out: each ABI passes one its own way +TEST(NamespacedStruct, ByValueStaysSkipped) +{ + auto out = cpp(std::string(gfx) + "extern \"C\" void fill(gfx::Color color);\n"); + EXPECT_THAT(out, HasSubstr("// skipped: fill \xE2\x80\x94 parameter 'color': struct passed by value")); +} diff --git a/tslang/unittests/TsClangImporter/NamingTest.cpp b/tslang/unittests/TsClangImporter/NamingTest.cpp index 1d59ec1fd..f765341fc 100644 --- a/tslang/unittests/TsClangImporter/NamingTest.cpp +++ b/tslang/unittests/TsClangImporter/NamingTest.cpp @@ -39,7 +39,7 @@ TEST(Naming, StripPrefixRenamesDeclarationsAndTheirUses) auto out = generate("struct fx_point { int x; };\nint fx_len(struct fx_point *p);\nint other(void);\n", options) .text; EXPECT_THAT(out, HasSubstr(" export type point = [x: s32];\n")); - EXPECT_THAT(out, HasSubstr(" @linkname(\"fx_len\") export declare function len(p: Reference): s32;\n")); + EXPECT_THAT(out, HasSubstr(" @linkname(\"fx_len\") export declare function len(p: Reference | null): s32;\n")); EXPECT_THAT(out, HasSubstr(" @linkname(\"other\") export declare function other(): s32;\n")); } diff --git a/tslang/unittests/TsClangImporter/SelectionTest.cpp b/tslang/unittests/TsClangImporter/SelectionTest.cpp index e380a6723..f2a0537e3 100644 --- a/tslang/unittests/TsClangImporter/SelectionTest.cpp +++ b/tslang/unittests/TsClangImporter/SelectionTest.cpp @@ -37,7 +37,7 @@ TEST(Selection, UsedTypesArePulledInFromAnyHeader) .text; EXPECT_THAT(out, HasSubstr("type Shared = [id: s32];")); EXPECT_THAT(out, HasSubstr("type sys_stat = [size: s32];")); - EXPECT_THAT(out, HasSubstr("declare function use(s: Reference, st: Reference): s32;")); + EXPECT_THAT(out, HasSubstr("declare function use(s: Reference | null, st: Reference | null): s32;")); EXPECT_THAT(out, Not(HasSubstr("other_unused"))); } @@ -49,7 +49,7 @@ TEST(Selection, FilterSelectsByCNameAnywhere) windowsTarget, files) .text; EXPECT_THAT(out, HasSubstr("declare function mine(): s32;")); - EXPECT_THAT(out, HasSubstr("declare function sys_call(s: Reference): s32;")); + EXPECT_THAT(out, HasSubstr("declare function sys_call(s: Reference | null): s32;")); EXPECT_THAT(out, HasSubstr("type sys_stat = [size: s32];")); EXPECT_THAT(out, Not(HasSubstr("yours"))); EXPECT_THAT(out, Not(HasSubstr("SYS_VALUE"))); diff --git a/tslang/unittests/TsClangImporter/SkipTest.cpp b/tslang/unittests/TsClangImporter/SkipTest.cpp index 06c448e23..984b25922 100644 --- a/tslang/unittests/TsClangImporter/SkipTest.cpp +++ b/tslang/unittests/TsClangImporter/SkipTest.cpp @@ -14,7 +14,7 @@ TEST(Skip, UnionIsSkippedButItsPointerUsersStay) { auto out = generate("union Value { int i; float f; };\nvoid set(union Value *v);\nvoid pass(union Value v);\n"); EXPECT_THAT(out.text, HasSubstr("// skipped: Value \xE2\x80\x94 union\ntype Value = Opaque;")); - EXPECT_THAT(out.text, HasSubstr("declare function set(v: Value): void;")); + EXPECT_THAT(out.text, HasSubstr("declare function set(v: Value | null): void;")); EXPECT_THAT(out.text, HasSubstr("// skipped: pass \xE2\x80\x94 parameter 'v': union passed by value")); EXPECT_THAT(out.warnings, Contains("skipped Value: union")); } diff --git a/tslang/unittests/TsClangImporter/TypeMappingTest.cpp b/tslang/unittests/TsClangImporter/TypeMappingTest.cpp index 6a0a41d9d..1f994733d 100644 --- a/tslang/unittests/TsClangImporter/TypeMappingTest.cpp +++ b/tslang/unittests/TsClangImporter/TypeMappingTest.cpp @@ -43,14 +43,14 @@ TEST(TypeMapping, FloatsAndBool) TEST(TypeMapping, CharPointersAreStrings) { EXPECT_THAT(text("const char *f(char *a, const char *b, char **c);\n"), - HasSubstr("declare function f(a: string, b: string, c: Reference): string;")); + HasSubstr("declare function f(a: string | null, b: string | null, c: Reference | null): string;")); } TEST(TypeMapping, PointerToScalarOrStructIsReference) { auto out = text("struct Point { int x; int y; };\nvoid f(int *a, struct Point *p, double *d);\n"); EXPECT_THAT(out, HasSubstr("type Point = [x: s32, y: s32];")); - EXPECT_THAT(out, HasSubstr("declare function f(a: Reference, p: Reference, d: Reference): void;")); + EXPECT_THAT(out, HasSubstr("declare function f(a: Reference | null, p: Reference | null, d: Reference | null): void;")); } TEST(TypeMapping, VoidPointerAndIncompleteStructAreOpaque) @@ -58,24 +58,26 @@ TEST(TypeMapping, VoidPointerAndIncompleteStructAreOpaque) auto out = text("typedef struct Counter Counter;\nvoid *f(void *a, Counter *c, struct Other *o);\n"); EXPECT_THAT(out, HasSubstr("type Counter = Opaque;")); EXPECT_THAT(out, HasSubstr("type Other = Opaque;")); - EXPECT_THAT(out, HasSubstr("declare function f(a: Opaque, c: Counter, o: Other): Opaque;")); + EXPECT_THAT(out, HasSubstr("declare function f(a: Opaque | null, c: Counter | null, o: Other | null): Opaque;")); } +// A C++ struct whose fields are where a tuple's are is a tuple, constructors and methods or not // (a polymorphic class is a @cpp class: CppClassTest.cpp) -TEST(TypeMapping, PointerToCxxClassIsOpaque) +TEST(TypeMapping, PointerToCxxStructIsReference) { auto out = generate("class Engine { public: Engine(); int power; };\n" "extern \"C\" int engine_power(Engine *e);\n", {}, windowsTarget, {}, "c++") .text; - EXPECT_THAT(out, HasSubstr("type Engine = Opaque;")); - EXPECT_THAT(out, HasSubstr("declare function engine_power(e: Engine): s32;")); + EXPECT_THAT(out, HasSubstr("type Engine = [power: s32];")); + EXPECT_THAT(out, HasSubstr("declare function engine_power(e: Reference | null): s32;")); } -// A class in a namespace or a template specialization has no file-scope TS declaration to name -// (found in the default library: regexp_* take a std::cmatch *), so a pointer to one is plain -// Opaque, with the C++ type in a comment - never a name the output does not declare. -TEST(TypeMapping, PointerToANamespacedOrTemplateClassIsPlainOpaque) +// A class in a namespace is declared under its own name (see NamespacedStructTest.cpp). A template +// specialization has no TS declaration to name (found in the default library: regexp_* take a +// std::cmatch *), so a pointer to one is plain Opaque, with the C++ type in a comment - never a name +// the output does not declare. +TEST(TypeMapping, PointerToATemplateClassIsPlainOpaque) { auto out = generate("namespace ns { class Engine { public: Engine(); }; }\n" "template struct Box { T value; };\n" @@ -83,9 +85,9 @@ TEST(TypeMapping, PointerToANamespacedOrTemplateClassIsPlainOpaque) "extern \"C\" int use(ns::Engine *e, Box *b, CharBox *c);\n", {}, windowsTarget, {}, "c++") .text; - EXPECT_THAT(out, HasSubstr("declare function use(e: Opaque /* ns::Engine */, b: Opaque /* Box */, " - "c: Opaque /* CharBox */): s32;")); - EXPECT_THAT(out, Not(HasSubstr("type Engine"))); + EXPECT_THAT(out, HasSubstr("// skipped: Engine \xE2\x80\x94 empty struct\ntype Engine = Opaque;")); + EXPECT_THAT(out, HasSubstr("declare function use(e: Engine | null, b: Opaque | null /* Box */, " + "c: Opaque | null /* CharBox */): s32;")); EXPECT_THAT(out, Not(HasSubstr("type Box"))); } @@ -93,9 +95,9 @@ TEST(TypeMapping, FunctionPointerIsOpaqueWithItsSignature) { auto out = text("int apply(int (*fn)(int, const char *), int v);\ntypedef void (*callback_t)(void *);\n" "void on(callback_t cb);\n"); - EXPECT_THAT(out, HasSubstr("declare function apply(fn: Opaque /* (p0: s32, p1: string) => s32 */, v: s32): s32;")); - EXPECT_THAT(out, HasSubstr("type callback_t = Opaque; // (p0: Opaque) => void")); - EXPECT_THAT(out, HasSubstr("declare function on(cb: callback_t): void;")); + EXPECT_THAT(out, HasSubstr("declare function apply(fn: Opaque | null /* (p0: s32, p1: string | null) => s32 */, v: s32): s32;")); + EXPECT_THAT(out, HasSubstr("type callback_t = Opaque; // (p0: Opaque | null) => void")); + EXPECT_THAT(out, HasSubstr("declare function on(cb: callback_t | null): void;")); EXPECT_THAT(out, HasSubstr("pass a function without captures, as `fn as Opaque`")); } @@ -104,7 +106,7 @@ TEST(TypeMapping, CompleteStructIsANamedTuple) EXPECT_THAT(text("#include \ntypedef struct { int8_t a; int32_t b; int64_t c; } Mixed;\n" "struct Node { int value; struct Node *next; };\n"), testing::AllOf(HasSubstr("type Mixed = [a: s8, b: s32, c: s64];"), - HasSubstr("type Node = [value: s32, next: Opaque /* Reference: a type cannot refer to itself */];"))); + HasSubstr("type Node = [value: s32, next: Opaque | null /* Reference: a type cannot refer to itself */];"))); } // tslang cannot declare a type that refers to itself (the compiler dies on one), so every pointer @@ -115,11 +117,11 @@ TEST(TypeMapping, TypeCyclesBecomeOpaque) auto out = text("struct A { struct B *b; int x; };\nstruct B { struct A *a; };\n" "typedef struct List *ListPtr;\nstruct List { ListPtr next; int v; };\n" "void walk(struct A *a, ListPtr l);\n"); - EXPECT_THAT(out, HasSubstr("type A = [b: Opaque /* Reference: a type cannot refer to itself */, x: s32];")); - EXPECT_THAT(out, HasSubstr("type B = [a: Opaque /* Reference: a type cannot refer to itself */];")); - EXPECT_THAT(out, HasSubstr("type List = [next: Opaque /* Reference: a type cannot refer to itself */, v: s32];")); + EXPECT_THAT(out, HasSubstr("type A = [b: Opaque | null /* Reference: a type cannot refer to itself */, x: s32];")); + EXPECT_THAT(out, HasSubstr("type B = [a: Opaque | null /* Reference: a type cannot refer to itself */];")); + EXPECT_THAT(out, HasSubstr("type List = [next: Opaque | null /* Reference: a type cannot refer to itself */, v: s32];")); EXPECT_THAT(out, HasSubstr("type ListPtr = Reference;")); - EXPECT_THAT(out, HasSubstr("declare function walk(a: Reference, l: ListPtr): void;")); + EXPECT_THAT(out, HasSubstr("declare function walk(a: Reference | null, l: ListPtr | null): void;")); EXPECT_THAT(out, Not(HasSubstr("fn as Opaque"))); } @@ -146,7 +148,7 @@ TEST(TypeMapping, FixedNonIntEnumIsATypeAndConstants) TEST(TypeMapping, VarargsKeepTheFixedParameters) { EXPECT_THAT(text("int print_all(const char *format, ...);\n"), - HasSubstr("@varargs declare function print_all(format: string): s32;")); + HasSubstr("@varargs declare function print_all(format: string | null): s32;")); } TEST(TypeMapping, LiteralMacrosAreConstants) @@ -177,7 +179,7 @@ TEST(TypeMapping, TypedefIsATypeAliasUnlessItNamesTheStruct) EXPECT_THAT(out, Not(HasSubstr("type Point = Point"))); EXPECT_THAT(out, HasSubstr("type Pt = Point;")); EXPECT_THAT(out, HasSubstr("type handle_t = u32;")); - EXPECT_THAT(out, HasSubstr("declare function f(p: Reference, h: handle_t): void;")); + EXPECT_THAT(out, HasSubstr("declare function f(p: Reference | null, h: handle_t): void;")); } TEST(TypeMapping, ParameterNames) @@ -196,7 +198,7 @@ TEST(TypeMapping, BuiltinTypeNamesAreNeverRedeclared) EXPECT_THAT(out, Not(HasSubstr("type boolean"))); EXPECT_THAT(out, HasSubstr("declare function is_ok(n: f64): s32;")); EXPECT_THAT(out, HasSubstr("type string_ = [len: s32];")); - EXPECT_THAT(out, HasSubstr("declare function use(s: Reference): void;")); + EXPECT_THAT(out, HasSubstr("declare function use(s: Reference | null): void;")); } // an enum that is declared but never defined has no enumerators to emit: its values are integers