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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Point>): f64;
declare function dist(p: Reference<Point> | 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 `/// <reference path="simple.ts" />`, 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
Expand All @@ -605,8 +607,8 @@ Include the result with `/// <reference path="simple.ts" />`, 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.
Expand Down Expand Up @@ -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<Point>): f64;
declare function dist(p: Reference<Point> | 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<T>`, 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

Expand All @@ -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;
Expand All @@ -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.
Expand Down
5 changes: 3 additions & 2 deletions tslang/include/TsClangImporter/BindingModel.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ struct TsType
std::vector<std::string> 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
Expand All @@ -46,7 +47,7 @@ std::string renderType(const TsType &type, const std::function<std::string(const
// The declaration keys `type` refers to, in order, each once.
void collectKeys(const TsType &type, std::vector<std::string> &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_`.
Expand Down Expand Up @@ -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<Method> methods; // CppClass: the methods it introduces, in declaration order
std::string fieldsSkipReason; // CppClass: non-empty when its fields aren't declared
Expand Down
9 changes: 7 additions & 2 deletions tslang/include/TsClangImporter/TypeMapper.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
Expand All @@ -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
Expand Down Expand Up @@ -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);
Expand Down
11 changes: 10 additions & 1 deletion tslang/lib/TsClangImporter/BindingModel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ TsType TsType::skip(std::string reason)

std::string renderType(const TsType &type, const std::function<std::string(const std::string &)> &nameOf)
{
if (type.nullable)
{
auto nonNull = type;
nonNull.nullable = false;
return renderType(nonNull, nameOf) + " | null";
}

switch (type.kind)
{
case TsType::Kind::Builtin:
Expand Down Expand Up @@ -86,7 +93,9 @@ void collectKeys(const TsType &type, std::vector<std::string> &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)
Expand Down
39 changes: 39 additions & 0 deletions tslang/lib/TsClangImporter/BindingPrinter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ llvm::Expected<PrintResult> printBindings(const HeaderModel &model, const PrintO
// name another declaration already has
llvm::StringMap<std::string> tsNames;
llvm::StringMap<std::string> taken;
llvm::StringMap<std::string> takenTypes; // a type's TS name -> its C++ name
for (size_t index = 0; index < model.decls.size(); ++index)
{
if (!selected[index])
Expand Down Expand Up @@ -172,7 +173,45 @@ llvm::Expected<PrintResult> 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<unsigned char>(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;
}

Expand Down
11 changes: 9 additions & 2 deletions tslang/lib/TsClangImporter/HeaderParser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -265,9 +265,16 @@ class Collector : public clang::RecursiveASTVisitor<Collector>
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;
}

Expand Down
56 changes: 52 additions & 4 deletions tslang/lib/TsClangImporter/TypeMapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,29 @@ std::string TypeMapper::tagName(const clang::TagDecl *tag)
return "";
}

std::string TypeMapper::qualifiedTagName(const clang::TagDecl *tag)
{
if (!llvm::isa<clang::CXXRecordDecl>(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)
Expand Down Expand Up @@ -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<int>'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<clang::CXXRecordDecl>(record);
// (a specialization only named, never instantiated, is TSK_Undeclared, hence the isa)
return !cxxRecord ||
(!cxxRecord->getDescribedClassTemplate() && !llvm::isa<clang::ClassTemplateSpecializationDecl>(cxxRecord));
(!cxxRecord->getDescribedClassTemplate() && !llvm::isa<clang::ClassTemplateSpecializationDecl>(cxxRecord) &&
cxxRecord->getTemplateSpecializationKind() == clang::TSK_Undeclared && !cxxRecord->isLambda());
}

bool TypeMapper::isSystem(const clang::Decl *decl)
Expand All @@ -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();

Expand Down Expand Up @@ -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<clang::CXXRecordDecl>(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())
Expand Down
2 changes: 2 additions & 0 deletions tslang/test/bindgen/bindgen_cpp_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ function main() {
const handle = shape.handle(); // a non-trivial one (@cppnontrivial)
print(handle.p == <Opaque>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));
Expand Down
1 change: 1 addition & 0 deletions tslang/test/bindgen/expected_cpp.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@
7 3
true
405
-988 3012
true false
5 changes: 5 additions & 0 deletions tslang/test/bindgen/fixture_cpp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
12 changes: 12 additions & 0 deletions tslang/test/bindgen/fixture_cpp.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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);
Expand Down
1 change: 1 addition & 0 deletions tslang/unittests/TsClangImporter/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ add_mlir_unittest(TsClangImporterTests
NamingTest.cpp
ParseErrorTest.cpp
CppClassTest.cpp
NamespacedStructTest.cpp
)

target_link_libraries(TsClangImporterTests
Expand Down
4 changes: 2 additions & 2 deletions tslang/unittests/TsClangImporter/CppClassTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<Point>, kind: u8, "
EXPECT_THAT(out, HasSubstr(" @slot(1) draw(other: Canvas, again: Canvas | null, at: Reference<Point>, 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;"));
Expand Down Expand Up @@ -161,7 +161,7 @@ TEST(CppClass, ClassesThatArentBound)
"extern \"C\" void use(Both *both, Virtual *v, Box<int> *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<int> */): void;"));
EXPECT_THAT(out, HasSubstr("declare function use(both: Both | null, v: Virtual | null, box: Opaque | null /* Box<int> */): void;"));
EXPECT_THAT(out, AllOf(HasSubstr("class A "), HasSubstr("class B ")));
}

Expand Down
Loading
Loading