diff --git a/docs/cpp-classes.md b/docs/cpp-classes.md index 87464e55a..136c9c496 100644 --- a/docs/cpp-classes.md +++ b/docs/cpp-classes.md @@ -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) | @@ -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 @@ -230,7 +242,9 @@ const log = new MessageLog(); // pass 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:** diff --git a/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h b/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h index df221a58b..baf90bb08 100644 --- a/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h +++ b/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h @@ -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(type)) + { + if (auto storageType = dyn_cast(classType.getStorageType()); + storageType && storageType.isCpp()) + { + return false; + } + } + // owns its own block. A Shared 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(type) || isa(type) || isa(type) || diff --git a/tslang/include/TypeScript/TypeScriptOps.h b/tslang/include/TypeScript/TypeScriptOps.h index de3b0885f..107ec07c5 100644 --- a/tslang/include/TypeScript/TypeScriptOps.h +++ b/tslang/include/TypeScript/TypeScriptOps.h @@ -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 diff --git a/tslang/include/TypeScript/TypeScriptTypes.td b/tslang/include/TypeScript/TypeScriptTypes.td index 21288e8e9..43e17985e 100644 --- a/tslang/include/TypeScript/TypeScriptTypes.td +++ b/tslang/include/TypeScript/TypeScriptTypes.td @@ -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"; diff --git a/tslang/lib/TypeScript/MLIRGenClasses.cpp b/tslang/lib/TypeScript/MLIRGenClasses.cpp index 8cdb4cd4f..c74a77e30 100644 --- a/tslang/lib/TypeScript/MLIRGenClasses.cpp +++ b/tslang/lib/TypeScript/MLIRGenClasses.cpp @@ -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(newClassPtr->classType.getStorageType()).setCpp(newClassPtr->isCppVTable); } // if this is generic specialized class then do not generate code for it @@ -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"; diff --git a/tslang/lib/TypeScript/TypeScriptDialect.cpp b/tslang/lib/TypeScript/TypeScriptDialect.cpp index e36003aec..efd0b102e 100644 --- a/tslang/lib/TypeScript/TypeScriptDialect.cpp +++ b/tslang/lib/TypeScript/TypeScriptDialect.cpp @@ -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 diff --git a/tslang/test/bindgen/run-bindgen-test.cmake b/tslang/test/bindgen/run-bindgen-test.cmake index 052a443d9..2eb9b51bf 100644 --- a/tslang/test/bindgen/run-bindgen-test.cmake +++ b/tslang/test/bindgen/run-bindgen-test.cmake @@ -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") diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 6d9139e5c..83825b714 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -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") @@ -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") @@ -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 @@ -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 $ --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 $ --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 $ --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 @@ -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 $ --emit=jit --no-default-lib -mm=none --shared-libs=$ @@ -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 $ --emit=jit --no-default-lib -mm=${cppvtable_mm} + --shared-libs=$ + --shared-libs=$ + "${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 $ --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. diff --git a/tslang/test/tester/cppvtable/abi_host.cpp b/tslang/test/tester/cppvtable/abi_host.cpp index 69a576dd0..4a869a6de 100644 --- a/tslang/test/tester/cppvtable/abi_host.cpp +++ b/tslang/test/tester/cppvtable/abi_host.cpp @@ -3,6 +3,7 @@ // ABI - MSVC's, or the Itanium one of g++ and clang - not a vtable made by hand in tslang. #include +#include #ifdef _WIN32 #define ABI_EXPORT extern "C" __declspec(dllexport) @@ -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(::operator new(sizeof(uint64_t) + sizeof(Methods))); + *reinterpret_cast(block) = GUARD; + return new (block + sizeof(uint64_t)) Methods(); +} + +ABI_EXPORT int abi_guard_intact(Methods *methods) +{ + return reinterpret_cast(methods)[-1] == GUARD ? 1 : 0; +} + +ABI_EXPORT void abi_delete_object(Methods *methods) +{ + methods->~Methods(); + ::operator delete(reinterpret_cast(methods) - sizeof(uint64_t)); +} + // whether p is &storage[index] ABI_EXPORT int abi_is_storage(void *p, int index) { diff --git a/tslang/test/tester/cppvtable/abi_itanium.ts b/tslang/test/tester/cppvtable/abi_itanium.ts index bd09cf04f..445e95f23 100644 --- a/tslang/test/tester/cppvtable/abi_itanium.ts +++ b/tslang/test/tester/cppvtable/abi_itanium.ts @@ -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 { @@ -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(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, big, (big - 0x1237)); @@ -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 = 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(heap) == 1, "and nothing once they are let go"); + abi_delete_object(heap); + print("done."); } diff --git a/tslang/test/tester/cppvtable/abi_msvc.ts b/tslang/test/tester/cppvtable/abi_msvc.ts index f6a55f08b..900821cfe 100644 --- a/tslang/test/tester/cppvtable/abi_msvc.ts +++ b/tslang/test/tester/cppvtable/abi_msvc.ts @@ -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 { @@ -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(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, big, (big - 0x1237)); @@ -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 = 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(heap) == 1, "and nothing once they are let go"); + abi_delete_object(heap); + print("done."); } diff --git a/tslang/test/tester/cppvtable/memory_model.ts b/tslang/test/tester/cppvtable/memory_model.ts index 76939473e..60043d420 100644 --- a/tslang/test/tester/cppvtable/memory_model.ts +++ b/tslang/test/tester/cppvtable/memory_model.ts @@ -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;