diff --git a/README.md b/README.md index 8c1727368..0f17e11d0 100644 --- a/README.md +++ b/README.md @@ -596,6 +596,13 @@ 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. +A struct is a named tuple with its fields where C puts them; tslang lays a nested tuple out in place, as C does a struct member: + +- **an array** `T x[N]` is a tuple of its elements, `x: [_0: T, _1: T, ...]` (tslang has no fixed-size array type; one of more than 1024 elements, or of unknown size, is skipped); +- **a union** is its first member when that alone has the union's size and alignment (`union { void *p; uint64_t n; }` is `p: Opaque | null`), raw words otherwise (`union { char buf[16]; char *ptr; }` is `[_0: u64, _1: u64]`); an anonymous one is named after its first member, an anonymous struct's fields are the struct's own; +- **a run of bitfields** is raw words over the bytes it uses, named after its first bitfield (`unsigned a : 1; unsigned b : 7;` is `a: [_0: u32]`): set and read the bits yourself; +- a struct with an `alignas` alignment beyond its fields' (`DECLSPEC_ALIGN(16)`), or packed, is skipped. + 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 @@ -675,7 +682,7 @@ declare function add(a: s32, b: s32): s32; declare function dist(p: Reference | null): f64; ``` -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. +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`). A derived struct has its bases' fields first, as C++ lays them out (an empty base has none; MSVC's tail padding after a base is a `_pad` field). A field holding a template's specialization by value declares the specialization, named by its arguments, so a library's types are laid out as that library has them: `std::string` is `basic_string_char` (MSVC's `[_Mypair: ...]` down to its `_Bx` buffer words, size and capacity; libc++'s `[__rep_: [_0: u64, _1: u64, _2: u64]]`), `nvrhi::static_vector` is `static_vector_IBindingSet_8` (its private `std::array`'s elements, then `current_size`), `RefCountPtr` is `RefCountPtr_IShader = [ptr_: IShader | null]`. Their member names are the C++ library's own, which differ between MSVC's and libc++; a pointer to a specialization alone stays `Opaque`. One that isn't a tuple is `type Name = Opaque;`, with the reason in a `// skipped:` line: a polymorphic class, a virtual base, a packed or over-aligned layout, or a field tsbindgen can't spell (`long double`, a huge array). 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 diff --git a/tslang/include/TsClangImporter/TypeMapper.h b/tslang/include/TsClangImporter/TypeMapper.h index 51224604e..2b9538523 100644 --- a/tslang/include/TsClangImporter/TypeMapper.h +++ b/tslang/include/TsClangImporter/TypeMapper.h @@ -11,6 +11,7 @@ namespace clang class Sema; } #include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/SmallPtrSet.h" #include "llvm/ADT/StringSet.h" #include @@ -43,13 +44,16 @@ class TypeMapper // itself through a pointer counts as mappable while it is being checked. std::string recordSkipReason(const clang::RecordDecl *record); - // A complete struct's fields. A pointer field that leads back to this struct is Opaque: - // tslang cannot declare a type that refers to itself. + // A complete struct's fields, as spellRecord lays them out. A pointer field that leads back to + // this struct is Opaque: tslang cannot declare a type that refers to itself. std::vector mapFields(const clang::RecordDecl *definition); - // The struct/enum's own name, or the typedef that names it when it has none ("" for neither). + // The struct/enum's own name, or the typedef that names it when it has none ("" for neither). A + // template's specialization is named by its arguments: `static_vector` is + // `static_vector_IBindingSet_8`. 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`) + // tagName with its namespaces and classes for a C++ struct out of the file scope (`nvrhi::Color`); + // a specialization's has its arguments (`nvrhi::static_vector`) static std::string qualifiedTagName(const clang::TagDecl *tag); static std::string recordKey(const clang::RecordDecl *record); static std::string enumKey(const clang::EnumDecl *enumDecl); @@ -63,8 +67,10 @@ class TypeMapper bool isPlainEnum(const clang::EnumDecl *enumDecl); // 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. + // a class too (declared under its own name), but not a template, nor a class local to a + // function. A template's specialization (std::string, nvrhi::static_vector) or a class + // inside one is declared once a field holds one by value, whose layout needs it; a pointer to + // one alone stays Opaque. 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 @@ -95,6 +101,31 @@ class TypeMapper } private: + // One field of a struct's tuple: its TS name and type, and where C++ puts it (bits). + struct Slot + { + std::string name; + TsType type; + uint64_t offset; + uint64_t size; + uint64_t align; + }; + + // The tuple a struct is, in layout order: the fields of its bases (non-virtual; an empty one has + // none), then its own. An array is a tuple of its elements; an anonymous struct's fields are + // the struct's; a union is its first member when that has the union's size and alignment, raw + // words otherwise; a run of bitfields raw words (named after the first); an empty + // [[no_unique_address]] field nothing. Returns why it isn't a tuple, or "". + std::string spellRecord(const clang::RecordDecl *definition, std::vector *fields); + // `baseEnds`: where each base's storage ends (bits), tail padding included + std::string collectSlots(const clang::RecordDecl *definition, uint64_t base, std::vector &slots, + std::vector &baseEnds); + Slot unionSlot(std::string name, const clang::RecordDecl *unionRecord, uint64_t offset); + // `[_0: u64, _1: u64, ...]`: `bytes` as words of the largest size up to `align` that fits + static TsType rawWords(uint64_t bytes, uint64_t align); + // a template's specialization, or a class inside one, complete: declarable when a field holds it + bool isInstantiated(const clang::RecordDecl *record); + TsType mapType(clang::QualType type, Use use); std::string referTo(const clang::TagDecl *tag); TsType mapPointer(clang::QualType pointee, Use use); @@ -111,6 +142,7 @@ class TypeMapper std::vector> referenced; llvm::StringSet<> referencedKeys; std::vector owners; // structs whose fields are being mapped + llvm::SmallPtrSet heldByValue; // instantiated records a field holds }; } // namespace tsbindgen diff --git a/tslang/lib/TsClangImporter/BindingModel.cpp b/tslang/lib/TsClangImporter/BindingModel.cpp index b7a9d89b7..9c41f9b71 100644 --- a/tslang/lib/TsClangImporter/BindingModel.cpp +++ b/tslang/lib/TsClangImporter/BindingModel.cpp @@ -94,7 +94,8 @@ std::string nameFromKey(const std::string &key) { auto colon = key.find(':'); auto name = colon == std::string::npos ? key : key.substr(colon + 1); - auto scope = name.rfind("::"); + // the scope before the template arguments: `nvrhi::static_vector` + auto scope = name.rfind("::", name.find('<')); return scope == std::string::npos ? name : name.substr(scope + 2); } diff --git a/tslang/lib/TsClangImporter/HeaderParser.cpp b/tslang/lib/TsClangImporter/HeaderParser.cpp index be2ea0397..d93bc76ff 100644 --- a/tslang/lib/TsClangImporter/HeaderParser.cpp +++ b/tslang/lib/TsClangImporter/HeaderParser.cpp @@ -360,8 +360,9 @@ class Collector : public clang::RecursiveASTVisitor return true; } - // C++ proper is v2: only what has C linkage has a C symbol - if (context.getLangOpts().CPlusPlus && !function->isExternC()) + // C++ proper is v2: only what has C linkage has a C symbol. An operator has no name to + // declare, C linkage or not (guiddef.h's operator== for GUID). + if (context.getLangOpts().CPlusPlus && (!function->isExternC() || !function->getIdentifier())) { return true; } diff --git a/tslang/lib/TsClangImporter/TypeMapper.cpp b/tslang/lib/TsClangImporter/TypeMapper.cpp index 5c4187623..b25f8ea80 100644 --- a/tslang/lib/TsClangImporter/TypeMapper.cpp +++ b/tslang/lib/TsClangImporter/TypeMapper.cpp @@ -7,8 +7,11 @@ #include "clang/Basic/TargetInfo.h" #include "clang/Sema/Sema.h" #include "llvm/ADT/SmallPtrSet.h" +#include "llvm/ADT/StringSet.h" #include "llvm/Support/MathExtras.h" +#include + namespace tsbindgen { @@ -21,6 +24,9 @@ bool isIndexTypedef(llvm::StringRef name) name == "uintptr_t"; } +// an array bigger than this is skipped rather than spelled as that many tuple fields +constexpr uint64_t maxArrayElements = 1024; + bool isPlainChar(clang::QualType canonical) { return canonical->isSpecificBuiltinType(clang::BuiltinType::Char_S) || @@ -29,8 +35,125 @@ bool isPlainChar(clang::QualType canonical) } // namespace +namespace +{ + +// The class is a template's specialization, or inside one (`basic_string::__rep`) +bool inInstantiation(const clang::TagDecl *tag) +{ + for (const clang::DeclContext *context = tag; context; context = context->getParent()) + { + if (llvm::isa(context)) + { + return true; + } + } + + return false; +} + +// `static_vector`, `basic_string::__rep`: the classes from the outermost +// enclosing one down, each with its template arguments (the defaulted ones left out); their +// namespaces too with `qualified` +std::string instantiationText(const clang::TagDecl *tag, bool qualified) +{ + auto policy = tag->getASTContext().getPrintingPolicy(); + policy.SuppressScope = !qualified; + policy.SuppressTagKeyword = true; + + std::vector chain; + const clang::DeclContext *context = tag; + for (; context && llvm::isa(context); context = context->getParent()) + { + chain.insert(chain.begin(), llvm::cast(context)); + } + + std::string text; + if (qualified && context) + { + if (auto *scope = llvm::dyn_cast(context)) + { + text = scope->getQualifiedNameAsString(); + } + } + + for (auto *part : chain) + { + text += (text.empty() ? "" : "::") + part->getName().str(); + if (auto *specialization = llvm::dyn_cast(part)) + { + std::string arguments; + llvm::raw_string_ostream stream(arguments); + clang::printTemplateArgumentList(stream, specialization->getTemplateArgs().asArray(), policy, + specialization->getSpecializedTemplate()->getTemplateParameters()); + stream.flush(); + // SuppressScope doesn't reach the arguments' types: drop each `scope::` there too + if (!qualified) + { + std::string unscoped; + for (size_t index = 0; index < arguments.size(); ++index) + { + if (arguments.compare(index, 2, "::") == 0) + { + while (!unscoped.empty() && (std::isalnum(static_cast(unscoped.back())) || + unscoped.back() == '_')) + { + unscoped.pop_back(); + } + + ++index; + continue; + } + + unscoped += arguments[index]; + } + + arguments = unscoped; + } + + text += arguments; + } + } + + return text; +} + +// a name of the identifier's characters: `static_vector` -> `static_vector_IBindingSet_8` +std::string identifierOf(const std::string &text) +{ + std::string name; + auto separated = false; + for (unsigned char c : text) + { + if (std::isalnum(c) || c == '_') + { + name += static_cast(c); + separated = false; + } + else if (!separated && !name.empty()) + { + name += '_'; + separated = true; + } + } + + if (separated) + { + name.pop_back(); + } + + return name; +} + +} // namespace + std::string TypeMapper::tagName(const clang::TagDecl *tag) { + if (inInstantiation(tag) && !tag->getName().empty()) + { + return identifierOf(instantiationText(tag, false)); + } + if (!tag->getName().empty()) { return tag->getName().str(); @@ -46,6 +169,11 @@ std::string TypeMapper::tagName(const clang::TagDecl *tag) std::string TypeMapper::qualifiedTagName(const clang::TagDecl *tag) { + if (inInstantiation(tag) && !tag->getName().empty()) + { + return instantiationText(tag, true); + } + if (!llvm::isa(tag) || tag->getDeclContext()->getRedeclContext()->isTranslationUnit()) { return tagName(tag); @@ -313,13 +441,18 @@ bool TypeMapper::isDeclarable(const clang::RecordDecl *record) return true; // in C, a struct declared inside another is still file-scoped } - // 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 + // a template, and a class inside a function or a lambda's, have no name the bindings could + // declare; a specialization or a class inside one is declared once a field holds it by value if (record->isDependentContext() || record->isInvalidDecl() || record->getParentFunctionOrMethod()) { return false; } + if (isInstantiated(record)) + { + return heldByValue.count(record->getDefinition()) > 0; + } + auto *cxxRecord = llvm::dyn_cast(record); // (a specialization only named, never instantiated, is TSK_Undeclared, hence the isa) return !cxxRecord || @@ -327,6 +460,16 @@ bool TypeMapper::isDeclarable(const clang::RecordDecl *record) cxxRecord->getTemplateSpecializationKind() == clang::TSK_Undeclared && !cxxRecord->isLambda()); } +bool TypeMapper::isInstantiated(const clang::RecordDecl *record) +{ + auto *definition = record->getDefinition(); + return context.getLangOpts().CPlusPlus && definition && !definition->isDependentContext() && + !definition->isInvalidDecl() && inInstantiation(definition) && !definition->getName().empty() && + (!llvm::isa(definition) || + llvm::cast(definition)->getSpecializationKind() != + clang::TSK_Undeclared); +} + bool TypeMapper::isSystem(const clang::Decl *decl) { auto &sourceManager = context.getSourceManager(); @@ -439,6 +582,13 @@ TsType TypeMapper::mapType(clang::QualType type, Use use) return TsType::skip(record->isUnion() ? "union passed by value" : "struct passed by value"); } + // a field holding a specialization (std::string, nvrhi::static_vector) needs its layout: + // it is declared, under a name of its arguments + if (use == Use::Field && isInstantiated(record)) + { + heldByValue.insert(record->getDefinition()); + } + if (!isDeclarable(record)) { return TsType::skip("C++ class '" + canonical.getAsString() + "'"); @@ -470,6 +620,36 @@ TsType TypeMapper::mapType(clang::QualType type, Use use) return TsType::named(referTo(record)); } + // `T x[N]`: a tuple of its N elements, `[_0: T, _1: T, ...]` (tslang has no fixed-size array + // type), which lays them out as C does + if (auto *array = context.getAsConstantArrayType(canonical); array && (use == Use::Field || use == Use::Alias)) + { + auto count = array->getSize().getZExtValue(); + if (count == 0) + { + return TsType::skip("array of no elements"); + } + + if (count > maxArrayElements) + { + return TsType::skip("array of " + std::to_string(count) + " elements"); + } + + auto element = map(array->getElementType(), Use::Field); + if (element.skipped()) + { + return TsType::skip("array of " + element.skipReason); + } + + std::vector names; + for (uint64_t index = 0; index < count; ++index) + { + names.push_back("_" + std::to_string(index)); + } + + return TsType::tuple(std::move(names), std::vector(count, element)); + } + if (canonical->isArrayType()) { return TsType::skip("array"); @@ -544,8 +724,9 @@ TsType TypeMapper::mapPointer(clang::QualType pointee, Use use) 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)) + // there is no declaration a name could refer to, so no name: the C++ type is a comment. A + // specialization is declared only for a field holding it, whatever the order it's met in. + if (isInstantiated(record) || !isDeclarable(record)) { return TsType::builtin("Opaque", pointee.getUnqualifiedType().getAsString()); } @@ -610,94 +791,269 @@ std::string TypeMapper::recordSkipReason(const clang::RecordDecl *record) // a struct that points to itself is mappable while this runs recordReasons[record] = ""; owners.push_back(record); + auto reason = spellRecord(record, nullptr); + owners.pop_back(); + recordReasons[record] = reason; + return reason; +} - auto reason = [&]() -> std::string { - if (record->isUnion()) +std::vector TypeMapper::mapFields(const clang::RecordDecl *definition) +{ + owners.push_back(definition); + std::vector fields; + spellRecord(definition, &fields); + owners.pop_back(); + return fields; +} + +TsType TypeMapper::rawWords(uint64_t bytes, uint64_t align) +{ + uint64_t word = 8; + while (word > 1 && (word > align || bytes % word != 0)) + { + word /= 2; + } + + std::vector names; + for (uint64_t index = 0; index < bytes / word; ++index) + { + names.push_back("_" + std::to_string(index)); + } + + auto type = TsType::builtin("u" + std::to_string(word * 8)); + return TsType::tuple(std::move(names), std::vector(bytes / word, type)); +} + +TypeMapper::Slot TypeMapper::unionSlot(std::string name, const clang::RecordDecl *unionRecord, uint64_t offset) +{ + const auto &layout = context.getASTRecordLayout(unionRecord); + auto size = static_cast(context.toBits(layout.getSize())); + auto align = static_cast(context.toBits(layout.getAlignment())); + + // its first member, when that alone has the union's size and alignment (`union { T *p; uint64_t + // n; }`); raw words otherwise (`union { char buf[16]; char *ptr; }`, a member with bitfields) + for (auto *member : unionRecord->fields()) + { + if (member->isZeroSize(context)) { - return "union"; + continue; } - // 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()) + if (!member->isBitField() && !member->isAnonymousStructOrUnion() && + context.getTypeSize(member->getType()) == size && context.getTypeAlign(member->getType()) == align) { - if (cxxRecord->isPolymorphic()) + auto type = map(member->getType(), Use::Field); + if (!type.skipped()) { - return "C++ class with virtual methods"; + return {name.empty() ? escapeReserved(member->getName().str()) : name, type, offset, size, align}; } + } - if (cxxRecord->getNumBases() > 0) - { - return "derived C++ class"; - } + break; + } + + return {name.empty() ? "_union" : name, rawWords(size / 8, align / 8), offset, size, align}; +} + +std::string TypeMapper::collectSlots(const clang::RecordDecl *definition, uint64_t base, std::vector &slots, + std::vector &baseEnds) +{ + const auto &layout = context.getASTRecordLayout(definition); + + // the bases' fields first, where the layout puts each base: C++ member lookup sees them as the + // struct's own (nvrhi::static_vector's private std::array) + if (auto *cxxRecord = llvm::dyn_cast(definition)) + { + if (cxxRecord->isPolymorphic()) + { + return "C++ class with virtual methods"; } - if (record->field_empty()) + if (cxxRecord->getNumVBases() > 0) { - return "empty struct"; + return "virtual base class"; } - // a named tuple is laid out with natural alignment, field after field; anything else would - // put the fields where C does not - const auto &layout = context.getASTRecordLayout(record); - uint64_t offset = 0; - uint64_t maxAlign = 8; - unsigned index = 0; - for (auto *field : record->fields()) + for (auto &baseSpecifier : cxxRecord->bases()) { - auto fieldName = field->getName().str(); - if (field->isBitField()) + auto *baseRecord = baseSpecifier.getType()->getAsCXXRecordDecl(); + if (!baseRecord || !baseRecord->getDefinition()) { - return "bitfield '" + fieldName + "'"; + return "incomplete base class"; } - if (fieldName.empty()) + baseRecord = baseRecord->getDefinition(); + if (baseRecord->isEmpty()) { - return "anonymous member"; + continue; // no storage (allocator, std::_Compressed_pair's empty base) } - auto type = map(field->getType(), Use::Field); - if (type.skipped()) + auto baseOffset = base + static_cast(context.toBits(layout.getBaseClassOffset(baseRecord))); + auto reason = collectSlots(baseRecord, baseOffset, slots, baseEnds); + if (!reason.empty()) { - return "field '" + fieldName + "': " + type.skipReason; + return "base class '" + baseRecord->getQualifiedNameAsString() + "': " + reason; } - auto align = context.getTypeAlign(field->getType()); - offset = llvm::alignTo(offset, align); - if (layout.getFieldOffset(index) != offset) + // MSVC puts the next field after the base's tail padding, the Itanium ABI may put it in + // there: spellRecord pads up to here when the next field is here + baseEnds.push_back(baseOffset + static_cast(context.toBits(context.getASTRecordLayout(baseRecord).getSize()))); + } + } + + std::vector fields(definition->field_begin(), definition->field_end()); + for (size_t index = 0; index < fields.size(); ++index) + { + auto *field = fields[index]; + auto offset = base + layout.getFieldOffset(field->getFieldIndex()); + auto fieldName = escapeReserved(field->getName().str()); + + // [[no_unique_address]] on an empty class (libc++'s std::string allocator): no storage + if (field->isZeroSize(context)) + { + continue; + } + + // a run of bitfields: raw words over the bytes they use, up to the next field + if (field->isBitField()) + { + auto start = offset / 8 * 8; + auto next = index + 1; + while (next < fields.size() && (fields[next]->isBitField() || fields[next]->isZeroSize(context))) { - return "packed or over-aligned layout"; + ++next; } - offset += context.getTypeSize(field->getType()); - maxAlign = std::max(maxAlign, align); - ++index; + auto end = next < fields.size() ? base + layout.getFieldOffset(fields[next]->getFieldIndex()) + : base + static_cast(context.toBits(layout.getDataSize())); + end = llvm::alignTo(end, 8); + auto align = static_cast(context.toBits(layout.getAlignment())) / 8; + auto words = rawWords((end - start) / 8, align); + // the words' alignment: their size, `u64` -> 64 bits + auto wordBits = static_cast(std::stoi(words.pointee.front().name.substr(1))); + slots.push_back({fieldName.empty() ? "_bits" : fieldName, words, start, end - start, wordBits}); + index = next - 1; + continue; } - if (llvm::alignTo(offset, maxAlign) != static_cast(context.toBits(layout.getSize()))) + auto canonical = field->getType().getCanonicalType(); + if (auto *fieldRecord = canonical->getAsRecordDecl()) { - return "packed or over-aligned layout"; + auto *fieldDefinition = fieldRecord->getDefinition(); + if (fieldDefinition && fieldDefinition->isUnion()) + { + slots.push_back(unionSlot(field->isAnonymousStructOrUnion() ? "" : fieldName, fieldDefinition, offset)); + continue; + } + + // an anonymous struct's fields are the struct's own, as in C11 + if (fieldDefinition && field->isAnonymousStructOrUnion()) + { + auto reason = collectSlots(fieldDefinition, offset, slots, baseEnds); + if (!reason.empty()) + { + return "anonymous member: " + reason; + } + + continue; + } } - return ""; - }(); + if (fieldName.empty()) + { + return "anonymous member"; + } - owners.pop_back(); - recordReasons[record] = reason; - return reason; + auto type = map(field->getType(), Use::Field); + if (type.skipped()) + { + return "field '" + fieldName + "': " + type.skipReason; + } + + slots.push_back({fieldName, type, offset, static_cast(context.getTypeSize(field->getType())), + static_cast(context.getTypeAlign(field->getType()))}); + } + + return ""; } -std::vector TypeMapper::mapFields(const clang::RecordDecl *definition) +std::string TypeMapper::spellRecord(const clang::RecordDecl *definition, std::vector *fields) { - owners.push_back(definition); - std::vector fields; - for (auto *field : definition->fields()) + if (definition->isUnion()) { - fields.push_back({escapeReserved(field->getName().str()), map(field->getType(), Use::Field)}); + return "union"; } - owners.pop_back(); - return fields; + std::vector slots; + std::vector ends; + auto reason = collectSlots(definition, 0, slots, ends); + if (!reason.empty()) + { + return reason; + } + + if (slots.empty()) + { + return "empty struct"; + } + + // a named tuple is laid out with natural alignment, field after field; a C++ layout that puts a + // field elsewhere (packed, over-aligned, the Itanium ABI's base tail padding) isn't one + uint64_t offset = 0; + uint64_t maxAlign = 8; + std::vector padded; + for (auto &slot : slots) + { + if (slot.offset > llvm::alignTo(offset, slot.align) && llvm::is_contained(ends, slot.offset)) + { + // words no bigger than the padding's start is aligned to + auto start = offset / 8; + auto words = rawWords((slot.offset - offset) / 8, start ? (start & (~start + 1)) : 8); + auto wordBits = static_cast(std::stoi(words.pointee.front().name.substr(1))); + padded.push_back({"_pad", words, offset, slot.offset - offset, wordBits}); + offset = slot.offset; + } + + offset = llvm::alignTo(offset, slot.align); + if (slot.offset != offset) + { + return "packed or over-aligned layout"; + } + + padded.push_back(slot); + offset += slot.size; + maxAlign = std::max(maxAlign, slot.align); + } + + slots = std::move(padded); + + // a tuple is aligned as its most aligned field; an alignas() struct (DECLSPEC_ALIGN(16)) more, so + // a struct holding one would put it elsewhere + const auto &layout = context.getASTRecordLayout(definition); + if (llvm::alignTo(offset, maxAlign) != static_cast(context.toBits(layout.getSize())) || + static_cast(context.toBits(layout.getAlignment())) > maxAlign) + { + return "packed or over-aligned layout"; + } + + if (fields) + { + // a base's field and the struct's own may have one name: the later gets `_2`, ... + llvm::StringSet<> names; + for (auto &slot : slots) + { + auto name = slot.name; + for (auto suffix = 2; names.count(name); ++suffix) + { + name = slot.name + "_" + std::to_string(suffix); + } + + names.insert(name); + fields->push_back({name, slot.type}); + } + } + + return ""; } const clang::RecordDecl *TypeMapper::recordBehind(clang::QualType type) diff --git a/tslang/test/bindgen/bindgen_cpp_test.ts b/tslang/test/bindgen/bindgen_cpp_test.ts index 88904847d..ced941118 100644 --- a/tslang/test/bindgen/bindgen_cpp_test.ts +++ b/tslang/test/bindgen/bindgen_cpp_test.ts @@ -23,4 +23,9 @@ function main() { const small = fx_make_square(2); print(square.bigger(small), small.bigger(square)); + + // fx::Batch: a SmallVec (its private base's array, then its count), a union as raw + // words ('A' in the buffer's first byte), a run of bitfields as a raw word (flags 3, mode 2) + let batch: Batch = [[[square, small, null], 2], [65, 0], [3 + 2 * 8], 1000]; + print(shape.sum(Ref(batch))); } diff --git a/tslang/test/bindgen/expected_cpp.txt b/tslang/test/bindgen/expected_cpp.txt index d89bfc013..7971399e3 100644 --- a/tslang/test/bindgen/expected_cpp.txt +++ b/tslang/test/bindgen/expected_cpp.txt @@ -8,3 +8,4 @@ true 405 -988 3012 true false +1281 diff --git a/tslang/test/bindgen/fixture_cpp.cpp b/tslang/test/bindgen/fixture_cpp.cpp index fcd50273f..7fe079baa 100644 --- a/tslang/test/bindgen/fixture_cpp.cpp +++ b/tslang/test/bindgen/fixture_cpp.cpp @@ -36,6 +36,17 @@ int32_t Shape::fits(const Extent &extent, const Extent *limit) return (limit ? limit->width : -1) * 1000 + extent.width * extent.height; } +int32_t Shape::sum(const Batch &batch) +{ + int32_t result = batch.total + batch.name.buf[0] + static_cast(batch.flags) + static_cast(batch.mode) * 100; + for (int64_t index = 0; index < batch.shapes.count; ++index) + { + result += batch.shapes.at(index)->area(); + } + + return result; +} + 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 5732b99bb..c8d7b3926 100644 --- a/tslang/test/bindgen/fixture_cpp.hpp +++ b/tslang/test/bindgen/fixture_cpp.hpp @@ -52,6 +52,38 @@ struct Extent } }; +class Shape; + +// a container shaped as nvrhi::static_vector: a private array base, then a count. A field holding +// one has tsbindgen declare the specialization (SmallVec_Shape_3). +template struct Storage +{ + T items[N]; +}; + +template struct SmallVec : private Storage +{ + int64_t count; + T at(int64_t index) const + { + return this->items[index]; + } +}; + +// with a union of a buffer and a pointer, as an std::string's, and a run of bitfields: raw words +struct Batch +{ + SmallVec shapes; + union + { + char buf[16]; + char *ptr; + } name; + uint32_t flags : 3; + uint32_t mode : 5; + int32_t total; +}; + class FX_API Shape { public: @@ -66,6 +98,7 @@ class FX_API Shape virtual Handle handle(); virtual Pair pair(int32_t x); virtual int32_t fits(const Extent &extent, const Extent *limit); + virtual int32_t sum(const Batch &batch); // 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 28a825f2b..17970d3a9 100644 --- a/tslang/unittests/TsClangImporter/CMakeLists.txt +++ b/tslang/unittests/TsClangImporter/CMakeLists.txt @@ -7,6 +7,7 @@ add_mlir_unittest(TsClangImporterTests ParseErrorTest.cpp CppClassTest.cpp NamespacedStructTest.cpp + LayoutTest.cpp ) target_link_libraries(TsClangImporterTests diff --git a/tslang/unittests/TsClangImporter/LayoutTest.cpp b/tslang/unittests/TsClangImporter/LayoutTest.cpp new file mode 100644 index 000000000..2e1ce1cdd --- /dev/null +++ b/tslang/unittests/TsClangImporter/LayoutTest.cpp @@ -0,0 +1,138 @@ +// How tsbindgen spells a struct's layout as a tuple: arrays, bitfields, unions, bases, and the +// specializations of templates a field holds (std::string, a static_vector, a RefCountPtr), with +// the shapes C++ libraries give them. Hermetic: the "library" types are written here, as MSVC's +// and libc++'s are shaped. + +#include "ImporterTestHelper.h" + +#include "gmock/gmock.h" + +using namespace tsbindgen_test; +using testing::HasSubstr; +using testing::Not; + +namespace +{ + +std::string cpp(const std::string &header, const std::string &target = windowsTarget) +{ + return generate(header, {}, target, {}, "c++").text; +} + +} // namespace + +// `T x[N]` is a tuple of its N elements +TEST(Layout, ArrayIsATupleOfItsElements) +{ + auto out = text("struct Item { int id; };\nstruct Grid { int cells[3]; struct Item items[2]; float m[2][2]; };\n"); + EXPECT_THAT(out, HasSubstr("type Grid = [cells: [_0: s32, _1: s32, _2: s32], items: [_0: Item, _1: Item], " + "m: [_0: [_0: f32, _1: f32], _1: [_0: f32, _1: f32]]];")); +} + +// A run of bitfields is raw words over its bytes, named after the first +TEST(Layout, BitfieldRunIsRawWords) +{ + auto out = text("struct Flags { unsigned a : 1; unsigned b : 7; int x; unsigned short c : 3; };\n"); + EXPECT_THAT(out, HasSubstr("type Flags = [a: [_0: u32], x: s32, c: [_0: u32]];")); +} + +// A union is its first member when that has the union's size and alignment, raw words otherwise; +// an anonymous one is named after its first member, an anonymous struct's fields are the struct's +TEST(Layout, Unions) +{ + auto out = text("#include \n" + "struct Str { union { char buf[16]; char *ptr; } bx; uint64_t size; uint64_t cap; };\n" + "struct Obj { union { void *pointer; uint64_t integer; }; };\n" + "struct Pt { struct { int x; int y; }; int z; };\n"); + EXPECT_THAT(out, HasSubstr("type Str = [bx: [_0: u64, _1: u64], size: u64, cap: u64];")); + EXPECT_THAT(out, HasSubstr("type Obj = [pointer: Opaque | null];")); + EXPECT_THAT(out, HasSubstr("type Pt = [x: s32, y: s32, z: s32];")); +} + +// A specialization a field holds is declared under a name of its arguments; its bases' fields come +// first, an empty base has none. A pointer to one alone stays Opaque. +TEST(Layout, SpecializationHeldByValue) +{ + const char *header = "#include \n" + "namespace lib {\n" + "template struct array { T elems[N]; };\n" + "template struct static_vector : private array {\n" + " size_t current_size; size_t size() const { return current_size; } };\n" + "template struct Ref { T *ptr_; Ref(); ~Ref(); };\n" + "template struct Other { T value; };\n" + "}\n" + "namespace app {\n" + "class Item { public: virtual ~Item(); virtual int id() = 0; };\n" + "struct State { lib::Ref owner; lib::static_vector items; };\n" + "}\n" + "extern \"C\" void use(const app::State *state, lib::Other *other);\n"; + for (auto *target : {windowsTarget, linuxTarget}) + { + auto out = cpp(header, target); + EXPECT_THAT(out, HasSubstr("type State = [owner: Ref_Item, items: static_vector_Item_2];")) << target; + EXPECT_THAT(out, HasSubstr("type Ref_Item = [ptr_: Item | null];")) << target; + EXPECT_THAT(out, HasSubstr("type static_vector_Item_2 = [elems: [_0: Item | null, _1: Item | null], current_size: index];")) + << target; + EXPECT_THAT(out, HasSubstr("use(state: Reference | null, other: Opaque | null /* lib::Other */)")) << target; + EXPECT_THAT(out, Not(HasSubstr("type Other"))) << target; + } +} + +// An std::string shaped as MSVC's: an empty allocator base (no storage), its value in a member +// whose union holds a char buffer or a pointer +TEST(Layout, MsvcShapedString) +{ + auto out = cpp("#include \n" + "namespace stdx {\n" + "template struct allocator {};\n" + "template struct compressed_pair : private A { V value; };\n" + "template struct string_val { union bxty { C buf[16]; C *ptr; } bx; size_t size; size_t res; };\n" + "template class basic_string { compressed_pair, string_val> pair; };\n" + "}\n" + "struct Desc { unsigned width; stdx::basic_string name; bool flag; };\n"); + EXPECT_THAT(out, HasSubstr("type Desc = [width: u32, name: basic_string_char, flag: boolean];")); + EXPECT_THAT(out, HasSubstr("type basic_string_char = [pair: compressed_pair_allocator_char_string_val_char];")); + EXPECT_THAT(out, HasSubstr("type compressed_pair_allocator_char_string_val_char = [value: string_val_char];")); + EXPECT_THAT(out, HasSubstr("type string_val_char = [bx: [_0: u64, _1: u64], size: index, res: index];")); +} + +// An std::string shaped as libc++'s: its representation a union of structs with bitfields (raw +// words), its allocator a [[no_unique_address]] member with no storage +TEST(Layout, LibcxxShapedString) +{ + auto out = cpp("#include \n" + "namespace stdx {\n" + "template struct allocator {};\n" + "template class basic_string {\n" + " struct longs { size_t is_long : 1; size_t cap : 63; size_t size; C *data; };\n" + " struct shorts { unsigned char is_long : 1; unsigned char size : 7; C data[23]; };\n" + " union rep { shorts s; longs l; };\n" + " rep rep_;\n" + " [[no_unique_address]] allocator alloc_;\n" + "};\n" + "}\n" + "struct Desc { unsigned width; stdx::basic_string name; };\n", + linuxTarget); + EXPECT_THAT(out, HasSubstr("type basic_string_char = [rep_: [_0: u64, _1: u64, _2: u64]];")); + EXPECT_THAT(out, HasSubstr("type Desc = [width: u32, name: basic_string_char];")); +} + +// MSVC puts a derived struct's fields after its base's tail padding (padded here), the Itanium ABI +// in it when the base isn't a POD +TEST(Layout, BaseTailPadding) +{ + const char *header = "#include \n" + "struct Base { Base(); int64_t x; char c; };\n" + "struct Derived : Base { int32_t y; };\n" + "extern \"C\" void use(Derived *d);\n"; + EXPECT_THAT(cpp(header, windowsTarget), + HasSubstr("type Derived = [x: s64, c: s8, _pad: [_0: u8, _1: u8, _2: u8, _3: u8, _4: u8, _5: u8, _6: u8], y: s32];")); + EXPECT_THAT(cpp(header, linuxTarget), HasSubstr("type Derived = [x: s64, c: s8, y: s32];")); +} + +// A base's field and the struct's own of one name: the later gets `_2` +TEST(Layout, InheritedNamesStayDistinct) +{ + auto out = cpp("struct A { int x; };\nstruct B : A { int x; };\nextern \"C\" void use(B *b);\n"); + EXPECT_THAT(out, HasSubstr("type B = [x: s32, x_2: s32];")); +} diff --git a/tslang/unittests/TsClangImporter/NamespacedStructTest.cpp b/tslang/unittests/TsClangImporter/NamespacedStructTest.cpp index 362f28380..6b49986bb 100644 --- a/tslang/unittests/TsClangImporter/NamespacedStructTest.cpp +++ b/tslang/unittests/TsClangImporter/NamespacedStructTest.cpp @@ -87,9 +87,9 @@ TEST(NamespacedStruct, NestedInAClass) 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) +// A derived struct has its base's fields first; a template's specialization a field holds is +// declared under a name of its arguments (LayoutTest.cpp) +TEST(NamespacedStruct, DerivedAndSpecializationFields) { auto out = cpp("template struct Vec { T *data; };\n" "namespace n {\n" @@ -98,10 +98,10 @@ TEST(NamespacedStruct, WhatStaysOpaque) "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"))); + EXPECT_THAT(out, HasSubstr("type Derived = [a: s32, b: s32];")); + EXPECT_THAT(out, HasSubstr("type Holder = [values: Vec_int];")); + EXPECT_THAT(out, HasSubstr("type Vec_int = [data: Reference | null];")); + EXPECT_THAT(out, HasSubstr("declare function use(d: Reference | null, h: Reference | null): void;")); } // A struct passed by value is still left out: each ABI passes one its own way diff --git a/tslang/unittests/TsClangImporter/NamingTest.cpp b/tslang/unittests/TsClangImporter/NamingTest.cpp index f765341fc..2681a9c1b 100644 --- a/tslang/unittests/TsClangImporter/NamingTest.cpp +++ b/tslang/unittests/TsClangImporter/NamingTest.cpp @@ -82,3 +82,13 @@ TEST(Naming, AsmLabelIsTheSymbol) EXPECT_THAT(text("int f(int x) __asm__(\"real_f\");\n"), HasSubstr("@linkname(\"real_f\") declare function f(x: s32): s32;")); } + +// an operator has no name to declare, C linkage or not (guiddef.h's operator== for GUID) +TEST(Naming, OperatorWithCLinkageIsLeftOut) +{ + auto out = generate("struct G { int a; };\nextern \"C\" { bool operator==(const G &x, const G &y); int other(void); }\n", + {}, windowsTarget, {}, "c++") + .text; + EXPECT_THAT(out, HasSubstr("declare function other(): s32;")); + EXPECT_THAT(out, testing::Not(HasSubstr("declare function ("))); +} diff --git a/tslang/unittests/TsClangImporter/SkipTest.cpp b/tslang/unittests/TsClangImporter/SkipTest.cpp index 984b25922..613ff2ca2 100644 --- a/tslang/unittests/TsClangImporter/SkipTest.cpp +++ b/tslang/unittests/TsClangImporter/SkipTest.cpp @@ -19,13 +19,14 @@ TEST(Skip, UnionIsSkippedButItsPointerUsersStay) EXPECT_THAT(out.warnings, Contains("skipped Value: union")); } -TEST(Skip, StructWithABitfieldOrAnUnmappableField) +// A field tsbindgen can't spell skips its struct; bitfields and arrays are spelled (LayoutTest.cpp) +TEST(Skip, StructWithAnUnmappableField) { - auto out = text("struct Flags { unsigned a : 1; };\nstruct Wide { long double x; };\n" - "struct Grid { int cells[4]; };\n"); - EXPECT_THAT(out, HasSubstr("// skipped: Flags \xE2\x80\x94 bitfield 'a'")); + auto out = text("struct Wide { long double x; };\nstruct Huge { char bytes[4096]; };\n" + "struct Flexible { int n; int items[]; };\n"); EXPECT_THAT(out, HasSubstr("// skipped: Wide \xE2\x80\x94 field 'x': long double")); - EXPECT_THAT(out, HasSubstr("// skipped: Grid \xE2\x80\x94 field 'cells': array")); + EXPECT_THAT(out, HasSubstr("// skipped: Huge \xE2\x80\x94 field 'bytes': array of 4096 elements")); + EXPECT_THAT(out, HasSubstr("// skipped: Flexible \xE2\x80\x94 field 'items': array")); } TEST(Skip, PackedStruct)