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
18 changes: 16 additions & 2 deletions docs/cpp-classes.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ errors and not warnings.
| `implements` | an implemented interface takes a slot |
| `extends` an ordinary class, or an ordinary class extending a `@cpp` one | the ordinary class's slots (`.instanceOf`, `.rtti`, ...) would shift the numbering |
| `instanceof`, a cast to or from `any`, a cast to an interface | each asks the object through slot 0, which is a C++ method |
| `-mm=rc` / `-mm=own` | retains and releases write a count in the word before an object, inside the C++ object |
| a 32-bit x86 MSVC target | C++ member functions are `__thiscall` there (`this` in ECX) |
| a virtual method returning a tuple, on a target other than MSVC and Itanium x86-64 / AArch64 | each ABI returns records its own way (see below) |
| on Itanium, a virtual method returning a trivial tuple with floating-point fields, unmarked | SysV / AAPCS64 return it in SSE registers / as an HFA, which isn't done (see below) |
Expand All @@ -87,6 +86,19 @@ errors and not warnings.

A generic or static class can't be `@cpp` either.

## Memory models

A `@cpp` class works under every memory model. Its reference is a C++ pointer, and no model
counts it:

- under `-mm=rc` and `-mm=own`, an ordinary object has its count in the word before it. A C++
object has no such word: the word before it belongs to the C++ heap, or to whatever is next to
it. So copying a `@cpp` reference into a variable, a field, an array or an argument doesn't
retain it, and dropping one doesn't release it. A field, array element or union member of a
`@cpp` type is skipped when what holds it is destroyed;
- the object is C++'s to free, the way the C++ library frees it (`Release()`, `delete`), as under
`gc` and `none`.

## Writing a binding

`tsbindgen` writes the binding from the C++ header, with the slots, the mangled symbols and the
Expand Down Expand Up @@ -230,7 +242,9 @@ const log = new MessageLog(); // pass <Opaque>log to C++
(`boolean`, `u8`, ...) is extended to 32 bits, as C++ callers expect. Declare by-value struct
parameters the way the ABI passes them (see "Writing a binding").
- **Lifetime.** C++ keeps a raw pointer that the collector can't see. Hold the object in tslang,
for example in a global, for as long as C++ may call it. A `string` argument is C++'s buffer:
for example in a global, for as long as C++ may call it. Under `-mm=rc` and `-mm=own` its
references aren't counted (see "Memory models"), so tslang never frees it: an object made with
`new` lives until the program ends, and so does what its fields hold. A `string` argument is C++'s buffer:
copy it to keep it past the call. (The other way, a `string` a C++ method returns to
TypeScript is copied by the call; declare the method to return `Opaque` for the pointer.)
- **Not yet supported:**
Expand Down
12 changes: 12 additions & 0 deletions tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h
Original file line number Diff line number Diff line change
Expand Up @@ -3956,6 +3956,18 @@ class MLIRTypeHelper
return false;
}

// A @cpp class's reference is a C++ pointer: no count sits in front of the object, and
// the object is C++'s to free (or, made by `new` here, never freed). Untracked, as a raw
// pointer is.
if (auto classType = dyn_cast<mlir_ts::ClassType>(type))
{
if (auto storageType = dyn_cast<mlir_ts::ClassStorageType>(classType.getStorageType());
storageType && storageType.isCpp())
{
return false;
}
}

// owns its own block. A Shared<T> handle (own spec 23) does too, and its block's payload
// is released by the handle's routine, so it is not a record here
if (isa<mlir_ts::StringType>(type) || isa<mlir_ts::ArrayType>(type) || isa<mlir_ts::ClassType>(type) ||
Expand Down
8 changes: 8 additions & 0 deletions tslang/include/TypeScript/TypeScriptOps.h
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,16 @@ struct ClassStorageTypeStorage : public ::mlir::TypeStorage
return success();
}

LogicalResult mutate(TypeStorageAllocator &, bool newCpp)
{
cpp = newCpp;
return success();
}

FlatSymbolRefAttr name;
::llvm::ArrayRef<::mlir::typescript::FieldInfo> fields;
// a @cpp class: its references are C++ pointers, which no memory model counts
bool cpp = false;
};
} // namespace detail

Expand Down
5 changes: 5 additions & 0 deletions tslang/include/TypeScript/TypeScriptTypes.td
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,11 @@ def TypeScript_ClassStorage : TypeScript_Type<"ClassStorage", [IsMutable]> {
::llvm::ArrayRef<::mlir::typescript::FieldInfo> getFields() const;
// Mutator
LogicalResult setFields(::llvm::ArrayRef<::mlir::typescript::FieldInfo> newFields);

// A @cpp class (docs/cpp-classes.md): a reference to it is a C++ pointer, with no count in
// front of the object, so it owns nothing in any memory model.
bool isCpp() const;
LogicalResult setCpp(bool cpp);
}];

let storageClass = "ClassStorageTypeStorage";
Expand Down
10 changes: 4 additions & 6 deletions tslang/lib/TypeScript/MLIRGenClasses.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,10 @@ namespace mlirgen
if (mlir::succeeded(mlirGenClassType(newClassPtr, genContext)))
{
newClassPtr->typeParamsWithArgs = genContext.typeParamsWithArgs;
// here, before anything of the type is generated: whether a reference owns anything
// (MLIRTypeHelper::ownsHeapMemory) is read off the type, and a retain placed while it
// still said yes would never pair with a release
mlir::cast<mlir_ts::ClassStorageType>(newClassPtr->classType.getStorageType()).setCpp(newClassPtr->isCppVTable);
}

// if this is generic specialized class then do not generate code for it
Expand Down Expand Up @@ -764,12 +768,6 @@ namespace mlirgen
return emitError(errorLocation);
};

if (compileOptions.tracksOwnership())
{
// retains and releases write a count in the word before an object, inside a C++ object
error(location) << "a @" CPP_VTABLE " class needs the gc or none memory model";
}

if (compileOptions.targetInfo.memberCallsUseThiscall)
{
error(location) << "a @" CPP_VTABLE " class can't be used on this target: its C++ methods are __thiscall";
Expand Down
10 changes: 10 additions & 0 deletions tslang/lib/TypeScript/TypeScriptDialect.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,16 @@ LogicalResult ClassStorageType::setFields(::llvm::ArrayRef<::mlir::typescript::F
return Base::mutate(newFields);
}

bool ClassStorageType::isCpp() const
{
return getImpl()->cpp;
}

LogicalResult ClassStorageType::setCpp(bool cpp)
{
return Base::mutate(cpp);
}

} // namespace typescript
} // namespace mlir
#endif
Expand Down
2 changes: 0 additions & 2 deletions tslang/test/bindgen/run-bindgen-test.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,6 @@ elseif(MODE STREQUAL "cpp")
set(bindgen_args "")
# no destructors, RTTI or exceptions in fixture_cpp: nothing of the C++ runtime is linked
set(clang_language -fno-rtti -fno-exceptions)
# a @cpp class needs the gc or none memory model
set(tslang_mm -mm=gc)
else()
set(bindings "fixture.ts")
set(program "bindgen_test.ts")
Expand Down
52 changes: 36 additions & 16 deletions tslang/test/tester/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -444,8 +444,11 @@ tslang_add_test(NAME test-compile-00-optional-loose-null COMMAND test-runner "${
tslang_add_test(NAME test-compile-00-union-nullable-array COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_nullable_array.ts")
tslang_add_test(NAME test-compile-00-union-nullable-value COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_nullable_value.ts")
tslang_add_test(NAME test-compile-00-opaque-nullable-compare COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00opaque_nullable_compare.ts")
# not in TSLANG_CORPUS: a @cpp class is an error under rc (cppvtable/memory_model.ts)
# in TSLANG_CORPUS too (rc, none), and under own here: a @cpp reference is counted by no model
tslang_add_test(NAME test-compile-00-cppvtable-class COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00cppvtable_class.ts")
tslang_add_test(NAME test-compile-own-00-cppvtable-class COMMAND test-runner -mm=own "${PROJECT_SOURCE_DIR}/test/tester/tests/00cppvtable_class.ts")
tslang_add_test(NAME test-compile-own-00-cpp-slot COMMAND test-runner -mm=own "${PROJECT_SOURCE_DIR}/test/tester/tests/00cpp_slot.ts")
tslang_add_test(NAME test-compile-own-00-cpp-overloads COMMAND test-runner -mm=own "${PROJECT_SOURCE_DIR}/test/tester/tests/00cpp_overloads.ts")
tslang_add_test(NAME test-compile-00-cpp-slot COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00cpp_slot.ts")
tslang_add_test(NAME test-compile-00-cpp-overloads COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00cpp_overloads.ts")
tslang_add_test(NAME test-compile-00-union-value-equality COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_value_equality.ts")
Expand Down Expand Up @@ -1012,6 +1015,9 @@ tslang_add_test(NAME test-jit-00-opaque-nullable-compare COMMAND test-runner -ji
tslang_add_test(NAME test-jit-00-cppvtable-class COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00cppvtable_class.ts")
tslang_add_test(NAME test-jit-00-cpp-slot COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00cpp_slot.ts")
tslang_add_test(NAME test-jit-00-cpp-overloads COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00cpp_overloads.ts")
tslang_add_test(NAME test-jit-own-00-cppvtable-class COMMAND test-runner -jit -mm=own "${PROJECT_SOURCE_DIR}/test/tester/tests/00cppvtable_class.ts")
tslang_add_test(NAME test-jit-own-00-cpp-slot COMMAND test-runner -jit -mm=own "${PROJECT_SOURCE_DIR}/test/tester/tests/00cpp_slot.ts")
tslang_add_test(NAME test-jit-own-00-cpp-overloads COMMAND test-runner -jit -mm=own "${PROJECT_SOURCE_DIR}/test/tester/tests/00cpp_overloads.ts")
tslang_add_test(NAME test-jit-00-union-value-equality COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_value_equality.ts")
tslang_add_test(NAME test-jit-00-strict-equals-mismatched-types COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00strict_equals_mismatched_types.ts")
tslang_add_test(NAME test-jit-00-any-number-compare COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00any_number_compare.ts")
Expand Down Expand Up @@ -2383,6 +2389,9 @@ set(TSLANG_CORPUS
00any_number_compare.ts
00narrow_array_null.ts
00opaque_nullable_compare.ts
00cppvtable_class.ts
00cpp_slot.ts
00cpp_overloads.ts
)

# the default model runs these ahead of time only, so the other models do too
Expand Down Expand Up @@ -3054,21 +3063,16 @@ foreach(cppvtable_error ${cppvtable_errors})
FAIL_REGULAR_EXPRESSION "Stack dump|Assertion failed")
endforeach()

# Retains and releases would write a count in the word before a C++ object: rc is an error, none
# compiles.
add_test(NAME test-compile-cppvtable-memory-model-rc
COMMAND $<TARGET_FILE:tslang> --emit=obj --no-default-lib -mm=rc
"${PROJECT_SOURCE_DIR}/test/tester/cppvtable/memory_model.ts"
-o "${CMAKE_CURRENT_BINARY_DIR}/cppvtable-memory-model-rc.obj")
set_tests_properties(test-compile-cppvtable-memory-model-rc
PROPERTIES PASS_REGULAR_EXPRESSION "memory_model\\.ts:3:1: error: a @cpp class needs the gc or none memory model"
FAIL_REGULAR_EXPRESSION "Stack dump|Assertion failed")
add_test(NAME test-compile-cppvtable-memory-model-none
COMMAND $<TARGET_FILE:tslang> --emit=obj --no-default-lib -mm=none
"${PROJECT_SOURCE_DIR}/test/tester/cppvtable/memory_model.ts"
-o "${CMAKE_CURRENT_BINARY_DIR}/cppvtable-memory-model-none.obj")
set_tests_properties(test-compile-cppvtable-memory-model-none
PROPERTIES FAIL_REGULAR_EXPRESSION "error:|Stack dump|Assertion failed")
# A @cpp reference is a C++ pointer, with no count in front of the object: every model compiles it,
# and rc and own count none of its copies (test-jit-cppvtable-abi-* below run it).
foreach(cppvtable_mm rc own none)
add_test(NAME test-compile-cppvtable-memory-model-${cppvtable_mm}
COMMAND $<TARGET_FILE:tslang> --emit=obj --no-default-lib -mm=${cppvtable_mm}
"${PROJECT_SOURCE_DIR}/test/tester/cppvtable/memory_model.ts"
-o "${CMAKE_CURRENT_BINARY_DIR}/cppvtable-memory-model-${cppvtable_mm}.obj")
set_tests_properties(test-compile-cppvtable-memory-model-${cppvtable_mm}
PROPERTIES FAIL_REGULAR_EXPRESSION "error:|Stack dump|Assertion failed")
endforeach()

# A method returning a record: tests/00cppvtable_hidden_return.ts's slots, made by hand, return it as
# MSVC does, through a pointer after `this`, so they run on Windows only; the Itanium ABI's way is
Expand Down Expand Up @@ -3164,6 +3168,7 @@ if (CMAKE_SIZEOF_VOID_P EQUAL 8 AND (MSVC OR (NOT WIN32 AND CMAKE_SYSTEM_PROCESS
else()
set(cppvtable_abi itanium)
endif()
# under every model: under rc and own nothing writes in front of the C++ object, which C++ deletes
add_test(NAME test-jit-cppvtable-abi
COMMAND $<TARGET_FILE:tslang> --emit=jit --no-default-lib -mm=none
--shared-libs=$<TARGET_FILE:TypeScriptRuntime>
Expand All @@ -3172,6 +3177,21 @@ if (CMAKE_SIZEOF_VOID_P EQUAL 8 AND (MSVC OR (NOT WIN32 AND CMAKE_SYSTEM_PROCESS
set_tests_properties(test-jit-cppvtable-abi
PROPERTIES PASS_REGULAR_EXPRESSION "done\\."
FAIL_REGULAR_EXPRESSION "error:|Stack dump|Segmentation|Assertion failed")
foreach(cppvtable_mm rc own)
add_test(NAME test-jit-cppvtable-abi-${cppvtable_mm}
COMMAND $<TARGET_FILE:tslang> --emit=jit --no-default-lib -mm=${cppvtable_mm}
--shared-libs=$<TARGET_FILE:TypeScriptRuntime>
--shared-libs=$<TARGET_FILE:tslang_cppvtable_abi>
"${PROJECT_SOURCE_DIR}/test/tester/cppvtable/abi_${cppvtable_abi}.ts")
set_tests_properties(test-jit-cppvtable-abi-${cppvtable_mm}
PROPERTIES PASS_REGULAR_EXPRESSION "done\\."
FAIL_REGULAR_EXPRESSION "error:|Stack dump|Segmentation|Assertion failed")
add_test(NAME test-${cppvtable_mm}-verify-ownership-cppvtable-abi
COMMAND $<TARGET_FILE:tslang> --emit=mlir-affine --no-default-lib -mm=${cppvtable_mm} --verify-ownership
"${PROJECT_SOURCE_DIR}/test/tester/cppvtable/abi_${cppvtable_abi}.ts" -o -)
set_tests_properties(test-${cppvtable_mm}-verify-ownership-cppvtable-abi
PROPERTIES FAIL_REGULAR_EXPRESSION "error:|warning:|Stack dump|Assertion failed")
endforeach()
endif()

# MSVC member functions are __thiscall on 32-bit x86, which a class method call isn't.
Expand Down
24 changes: 24 additions & 0 deletions tslang/test/tester/cppvtable/abi_host.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// ABI - MSVC's, or the Itanium one of g++ and clang - not a vtable made by hand in tslang.

#include <cstdint>
#include <new>

#ifdef _WIN32
#define ABI_EXPORT extern "C" __declspec(dllexport)
Expand Down Expand Up @@ -134,6 +135,29 @@ ABI_EXPORT Methods *abi_object()
return &methods;
}

// on the C++ heap, right after a word of C++'s own: under rc and own, a count tslang kept for it
// would be in that word. A retain and its release leave it as it was, so it is checked while
// tslang holds the copies (abi_guard_intact), and then C++ deletes the object.
static const uint64_t GUARD = 0x5a5a5a5a5a5a5a5aull;

ABI_EXPORT Methods *abi_heap_object()
{
auto block = static_cast<char *>(::operator new(sizeof(uint64_t) + sizeof(Methods)));
*reinterpret_cast<uint64_t *>(block) = GUARD;
return new (block + sizeof(uint64_t)) Methods();
}

ABI_EXPORT int abi_guard_intact(Methods *methods)
{
return reinterpret_cast<uint64_t *>(methods)[-1] == GUARD ? 1 : 0;
}

ABI_EXPORT void abi_delete_object(Methods *methods)
{
methods->~Methods();
::operator delete(reinterpret_cast<char *>(methods) - sizeof(uint64_t));
}

// whether p is &storage[index]
ABI_EXPORT int abi_is_storage(void *p, int index)
{
Expand Down
26 changes: 26 additions & 0 deletions tslang/test/tester/cppvtable/abi_itanium.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ type R12 = [a: u32, b: u32, c: u32];

declare function abi_object(): Opaque;
declare function abi_is_storage(p: Opaque | null, index: s32): s32;
declare function abi_heap_object(): Opaque;
declare function abi_delete_object(methods: Opaque): void;
declare function abi_guard_intact(methods: Opaque): s32;

@cpp
class Methods {
Expand Down Expand Up @@ -101,6 +104,21 @@ declare function abi_call_callback(callback: Opaque): s32;
declare function abi_callback_is_even(callback: Opaque, x: s32): s32;
declare function abi_callback_pick(callback: Opaque): s32;

// A @cpp reference copied into a field, an array and a local, each given up at the end: under rc
// and own none of them is counted, since the object has no count in front of it. The word before
// it is C++'s (abi_host.cpp's guard), and stays as C++ wrote it while the copies are held.
class Holder {
constructor(public methods: Methods) {}
}

function copies(methods: Methods): s32 {
const held = new Holder(methods);
const list = [methods, methods];
let other: Methods | null = methods;
assert(abi_guard_intact(<Opaque>methods) == 1, "nothing written in the word before a C++ object");
return held.methods.plain(2, 3) + list[1].plain(1, 1) + (other ? other.plain(0, 0) : 0);
}

// arguments made from a wider value: the register's upper bits aren't zero
function callNarrow(methods: Methods, big: s32): s32 {
return methods.narrow(big > 0, <u8>big, <s8>(big - 0x1237));
Expand Down Expand Up @@ -147,5 +165,13 @@ function main() {
const r12 = methods.r12(7);
assert(r12.a == 7 && r12.b == 14 && r12.c == 21, "a 12-byte record returned");

// an object on the C++ heap, which C++ deletes: a count written in front of it breaks the delete
const heap = <Methods>abi_heap_object();
for (let i = 0; i < 3; i++) {
assert(copies(heap) == 28, "a C++ object held and let go, uncounted");
}
assert(abi_guard_intact(<Opaque>heap) == 1, "and nothing once they are let go");
abi_delete_object(<Opaque>heap);

print("done.");
}
26 changes: 26 additions & 0 deletions tslang/test/tester/cppvtable/abi_msvc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ type R12 = [a: u32, b: u32, c: u32];

declare function abi_object(): Opaque;
declare function abi_is_storage(p: Opaque | null, index: s32): s32;
declare function abi_heap_object(): Opaque;
declare function abi_delete_object(methods: Opaque): void;
declare function abi_guard_intact(methods: Opaque): s32;

@cpp
class Methods {
Expand Down Expand Up @@ -98,6 +101,21 @@ declare function abi_call_callback(callback: Opaque): s32;
declare function abi_callback_is_even(callback: Opaque, x: s32): s32;
declare function abi_callback_pick(callback: Opaque): s32;

// A @cpp reference copied into a field, an array and a local, each given up at the end: under rc
// and own none of them is counted, since the object has no count in front of it. The word before
// it is C++'s (abi_host.cpp's guard), and stays as C++ wrote it while the copies are held.
class Holder {
constructor(public methods: Methods) {}
}

function copies(methods: Methods): s32 {
const held = new Holder(methods);
const list = [methods, methods];
let other: Methods | null = methods;
assert(abi_guard_intact(<Opaque>methods) == 1, "nothing written in the word before a C++ object");
return held.methods.plain(2, 3) + list[1].plain(1, 1) + (other ? other.plain(0, 0) : 0);
}

// arguments made from a wider value: the register's upper bits aren't zero
function callNarrow(methods: Methods, big: s32): s32 {
return methods.narrow(big > 0, <u8>big, <s8>(big - 0x1237));
Expand Down Expand Up @@ -144,5 +162,13 @@ function main() {
const r12 = methods.r12(7);
assert(r12.a == 7 && r12.b == 14 && r12.c == 21, "a 12-byte record returned");

// an object on the C++ heap, which C++ deletes: a count written in front of it breaks the delete
const heap = <Methods>abi_heap_object();
for (let i = 0; i < 3; i++) {
assert(copies(heap) == 28, "a C++ object held and let go, uncounted");
}
assert(abi_guard_intact(<Opaque>heap) == 1, "and nothing once they are let go");
abi_delete_object(<Opaque>heap);

print("done.");
}
4 changes: 2 additions & 2 deletions tslang/test/tester/cppvtable/memory_model.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Compiled with -mm=rc: retains and releases would write the count in the word before a C++
// object. The same file compiles with -mm=none.
// Compiled under rc, own and none: a @cpp reference is a C++ pointer, which no model counts, so
// nothing is written in the word before the C++ object.
@cpp
class Base {
@slot(0) first(): s32;
Expand Down
Loading