From 6c64011a2204deedf9dd63c0c187a78ea5984527 Mon Sep 17 00:00:00 2001 From: Aaron Jomy Date: Wed, 21 May 2025 17:33:24 +0200 Subject: [PATCH 1/2] Add support for array of objects in Construct/Destruct --- include/clang-c/CXCppInterOp.h | 8 ++- include/clang/Interpreter/CppInterOp.h | 25 ++++++--- lib/Interpreter/CXCppInterOp.cpp | 16 +++--- lib/Interpreter/CppInterOp.cpp | 76 ++++++++++++++++++++------ 4 files changed, 88 insertions(+), 37 deletions(-) diff --git a/include/clang-c/CXCppInterOp.h b/include/clang-c/CXCppInterOp.h index f885e19c5..bb10c0ddd 100644 --- a/include/clang-c/CXCppInterOp.h +++ b/include/clang-c/CXCppInterOp.h @@ -332,7 +332,8 @@ CINDEX_LINKAGE void clang_deallocate(CXObject address); * Creates an object of class \c scope and calls its default constructor. If \c * arena is set it uses placement new. */ -CINDEX_LINKAGE CXObject clang_construct(CXScope scope, void* arena); +CINDEX_LINKAGE CXObject clang_construct(CXScope scope, void* arena, + size_t count); /** * Creates a trampoline function and makes a call to a generic function or @@ -349,7 +350,7 @@ CINDEX_LINKAGE CXObject clang_construct(CXScope scope, void* arena); * \param self The 'this pointer' of the object. */ CINDEX_LINKAGE void clang_invoke(CXScope func, void* result, void** args, - size_t n, void* self); + size_t n, size_t nary, void* self); /** * Calls the destructor of object of type \c type. When withFree is true it @@ -361,7 +362,8 @@ CINDEX_LINKAGE void clang_invoke(CXScope func, void* result, void** args, * * \param withFree Whether to call operator delete/free or not. */ -CINDEX_LINKAGE void clang_destruct(CXObject This, CXScope S, bool withFree); +CINDEX_LINKAGE void clang_destruct(CXObject This, CXScope S, + bool withFree = true, size_t nary = 0UL); /** * @} diff --git a/include/clang/Interpreter/CppInterOp.h b/include/clang/Interpreter/CppInterOp.h index 7d1fdb12a..301a67242 100644 --- a/include/clang/Interpreter/CppInterOp.h +++ b/include/clang/Interpreter/CppInterOp.h @@ -111,7 +111,10 @@ namespace Cpp { // FIXME: Figure out how to unify the wrapper signatures. // FIXME: Hide these implementation details by moving wrapper generation in // this class. - using GenericCall = void (*)(void*, int, void**, void*); + + // (self, args, args, result, nary) + using GenericCall = void (*)(void*, int, void**, void*, size_t); + // (self, nary, withFree) using DestructorCall = void (*)(void*, unsigned long, int); private: union { @@ -156,16 +159,17 @@ namespace Cpp { // self can go in the end and be nullptr by default; result can be a nullptr // by default. These changes should be synchronized with the wrapper if we // decide to directly. - void Invoke(void* result, ArgList args = {}, void* self = nullptr) const { + void Invoke(void* result, ArgList args = {}, void* self = nullptr, + size_t nary = 0UL) const { // Forward if we intended to call a dtor with only 1 parameter. if (m_Kind == kDestructorCall && result && !args.m_Args) - return InvokeDestructor(result, /*nary=*/0UL, /*withFree=*/true); + return InvokeDestructor(result, nary, /*withFree=*/true); #ifndef NDEBUG assert(AreArgumentsValid(result, args, self) && "Invalid args!"); ReportInvokeStart(result, args, self); #endif // NDEBUG - m_GenericCall(self, args.m_ArgSize, args.m_Args, result); + m_GenericCall(self, args.m_ArgSize, args.m_Args, result, nary); } /// Makes a call to a destructor. ///\param[in] object - the pointer of the object whose destructor we call. @@ -753,20 +757,25 @@ namespace Cpp { CPPINTEROP_API std::vector GetDimensions(TCppType_t type); /// Allocates memory for a given class. - CPPINTEROP_API TCppObject_t Allocate(TCppScope_t scope); + /// \c count is used to indicate the number of objects to allocate for. + CPPINTEROP_API TCppObject_t Allocate(TCppScope_t scope, + TCppIndex_t count = 1UL); /// Deallocates memory for a given class. - CPPINTEROP_API void Deallocate(TCppScope_t scope, TCppObject_t address); + CPPINTEROP_API void Deallocate(TCppScope_t scope, TCppObject_t address, + TCppIndex_t count = 1UL); /// Creates an object of class \c scope and calls its default constructor. If /// \c arena is set it uses placement new. + /// \c count is used to indicate the number of objects to construct. CPPINTEROP_API TCppObject_t Construct(TCppScope_t scope, - void* arena = nullptr); + void* arena = nullptr, + TCppIndex_t count = 1UL); /// Calls the destructor of object of type \c type. When withFree is true it /// calls operator delete/free. CPPINTEROP_API void Destruct(TCppObject_t This, TCppScope_t type, - bool withFree = true); + bool withFree = true, TCppIndex_t count = 0UL); /// @name Stream Redirection /// diff --git a/lib/Interpreter/CXCppInterOp.cpp b/lib/Interpreter/CXCppInterOp.cpp index 031096c91..86f4f480e 100644 --- a/lib/Interpreter/CXCppInterOp.cpp +++ b/lib/Interpreter/CXCppInterOp.cpp @@ -598,25 +598,25 @@ void clang_deallocate(CXObject address) { ::operator delete(address); } namespace Cpp { void* Construct(compat::Interpreter& interp, TCppScope_t scope, - void* arena /*=nullptr*/); + void* arena /*=nullptr*/, TCppIndex_t count); } // namespace Cpp -CXObject clang_construct(CXScope scope, void* arena) { +CXObject clang_construct(CXScope scope, void* arena, size_t count) { return Cpp::Construct(*getInterpreter(scope), - static_cast(getDecl(scope)), arena); + static_cast(getDecl(scope)), arena, count); } void clang_invoke(CXScope func, void* result, void** args, size_t n, - void* self) { + size_t nary, void* self) { Cpp::MakeFunctionCallable(getInterpreter(func), getDecl(func)) - .Invoke(result, {args, n}, self); + .Invoke(result, {args, n}, self, nary); } namespace Cpp { void Destruct(compat::Interpreter& interp, TCppObject_t This, - clang::Decl* Class, bool withFree); + clang::Decl* Class, bool withFree, size_t nary); } // namespace Cpp -void clang_destruct(CXObject This, CXScope S, bool withFree) { - Cpp::Destruct(*getInterpreter(S), This, getDecl(S), withFree); +void clang_destruct(CXObject This, CXScope S, bool withFree, size_t nary) { + Cpp::Destruct(*getInterpreter(S), This, getDecl(S), withFree, nary); } \ No newline at end of file diff --git a/lib/Interpreter/CppInterOp.cpp b/lib/Interpreter/CppInterOp.cpp index 87eb63bff..a4808b00c 100755 --- a/lib/Interpreter/CppInterOp.cpp +++ b/lib/Interpreter/CppInterOp.cpp @@ -1896,13 +1896,19 @@ namespace Cpp { void make_narg_ctor(const FunctionDecl* FD, const unsigned N, std::ostringstream& typedefbuf, std::ostringstream& callbuf, - const std::string& class_name, int indent_level) { + const std::string& class_name, int indent_level, + bool array = false) { // Make a code string that follows this pattern: // // ClassName(args...) + // OR + // ClassName[nary](args...) // array of objects // - callbuf << class_name << "("; + callbuf << class_name; + if (array) + callbuf << "[nary]"; + callbuf << "("; for (unsigned i = 0U; i < N; ++i) { const ParmVarDecl* PVD = FD->getParamDecl(i); QualType Ty = PVD->getType(); @@ -2085,9 +2091,15 @@ namespace Cpp { std::ostringstream& buf, int indent_level) { // Make a code string that follows this pattern: // - // (*(ClassName**)ret) = (obj) ? - // new (*(ClassName**)ret) ClassName(args...) : new ClassName(args...); - // + // // array of objects construction + // if (nary > 1) { + // (*(ClassName**)ret) = (obj) ? new (*(ClassName**)ret) + // ClassName[nary](args...) : new ClassName[nary](args...); + // } + // else { + // (*(ClassName**)ret) = (obj) ? new (*(ClassName**)ret) + // ClassName(args...) : new ClassName(args...); + // } { std::ostringstream typedefbuf; std::ostringstream callbuf; @@ -2095,6 +2107,27 @@ namespace Cpp { // Write the return value assignment part. // indent(callbuf, indent_level); + callbuf << "if (nary > 1) {\n"; + indent(callbuf, indent_level); + callbuf << "(*(" << class_name << "**)ret) = "; + callbuf << "(obj) ? new (*(" << class_name << "**)ret) "; + make_narg_ctor(FD, N, typedefbuf, callbuf, class_name, indent_level, + true); + + callbuf << ": new "; + // + // Write the actual expression. + // + make_narg_ctor(FD, N, typedefbuf, callbuf, class_name, indent_level, + true); + // + // End the new expression statement. + // + callbuf << ";\n"; + indent(callbuf, indent_level); + callbuf << "}\n"; + callbuf << "else {\n"; + indent(callbuf, indent_level); callbuf << "(*(" << class_name << "**)ret) = "; callbuf << "(obj) ? new (*(" << class_name << "**)ret) "; make_narg_ctor(FD, N, typedefbuf, callbuf, class_name, indent_level); @@ -2108,6 +2141,8 @@ namespace Cpp { // End the new expression statement. // callbuf << ";\n"; + indent(callbuf, --indent_level); + callbuf << "}\n"; // // Output the whole new expression and return statement. // @@ -2626,7 +2661,8 @@ namespace Cpp { "__attribute__((annotate(\"__cling__ptrcheck(off)\")))\n" "extern \"C\" void "; buf << wrapper_name; - buf << "(void* obj, int nargs, void** args, void* ret)\n" + buf << "(void* obj, int nargs, void** args, void* ret, unsigned long " + "nary)\n" "{\n"; ++indent_level; if (min_args == num_params) { @@ -3577,17 +3613,18 @@ namespace Cpp { } } - TCppObject_t Allocate(TCppScope_t scope) { - return (TCppObject_t)::operator new(Cpp::SizeOf(scope)); + TCppObject_t Allocate(TCppScope_t scope, TCppIndex_t count) { + return (TCppObject_t)::operator new(Cpp::SizeOf(scope) * count); } - void Deallocate(TCppScope_t scope, TCppObject_t address) { - ::operator delete(address); + void Deallocate(TCppScope_t scope, TCppObject_t address, TCppIndex_t count) { + size_t bytes = Cpp::SizeOf(scope) * count; + ::operator delete(address, bytes); } // FIXME: Add optional arguments to the operator new. TCppObject_t Construct(compat::Interpreter& interp, TCppScope_t scope, - void* arena /*=nullptr*/) { + void* arena /*=nullptr*/, TCppIndex_t count /*=1UL*/) { auto* Class = (Decl*) scope; // FIXME: Diagnose. if (!HasDefaultConstructor(Class)) @@ -3596,7 +3633,8 @@ namespace Cpp { auto* const Ctor = GetDefaultConstructor(interp, Class); if (JitCall JC = MakeFunctionCallable(&interp, Ctor)) { if (arena) { - JC.Invoke(&arena, {}, (void*)~0); // Tell Invoke to use placement new. + JC.Invoke(&arena, {}, (void*)~0, + count); // Tell Invoke to use placement new. return arena; } @@ -3607,22 +3645,24 @@ namespace Cpp { return nullptr; } - TCppObject_t Construct(TCppScope_t scope, void* arena /*=nullptr*/) { - return Construct(getInterp(), scope, arena); + TCppObject_t Construct(TCppScope_t scope, void* arena /*=nullptr*/, + TCppIndex_t count /*=0UL*/) { + return Construct(getInterp(), scope, arena, count); } void Destruct(compat::Interpreter& interp, TCppObject_t This, Decl* Class, - bool withFree) { + bool withFree, TCppIndex_t nary) { if (auto wrapper = make_dtor_wrapper(interp, Class)) { - (*wrapper)(This, /*nary=*/0, withFree); + (*wrapper)(This, nary, withFree); return; } // FIXME: Diagnose. } - void Destruct(TCppObject_t This, TCppScope_t scope, bool withFree /*=true*/) { + void Destruct(TCppObject_t This, TCppScope_t scope, bool withFree /*=true*/, + TCppIndex_t count /*=1UL*/) { auto* Class = static_cast(scope); - Destruct(getInterp(), This, Class, withFree); + Destruct(getInterp(), This, Class, withFree, count); } class StreamCaptureInfo { From 624bfb15d9ad81d89658a68d050efabba45bd237 Mon Sep 17 00:00:00 2001 From: Aaron Jomy Date: Wed, 21 May 2025 17:34:49 +0200 Subject: [PATCH 2/2] Add tests for ConstructArray, DestructArray, Allocate and Deallocate --- .../CppInterOp/FunctionReflectionTest.cpp | 133 +++++++++++++++++- 1 file changed, 130 insertions(+), 3 deletions(-) diff --git a/unittests/CppInterOp/FunctionReflectionTest.cpp b/unittests/CppInterOp/FunctionReflectionTest.cpp index 7448cbe30..ad704959b 100644 --- a/unittests/CppInterOp/FunctionReflectionTest.cpp +++ b/unittests/CppInterOp/FunctionReflectionTest.cpp @@ -1462,7 +1462,7 @@ TEST(FunctionReflectionTest, JitCallAdvanced) { auto* I = clang_createInterpreterFromRawPtr(Cpp::GetInterpreter()); auto S = clang_getDefaultConstructor(make_scope(Decls[0], I)); void* object_c = nullptr; - clang_invoke(S, &object_c, nullptr, 0, nullptr); + clang_invoke(S, &object_c, nullptr, 0, 0UL, nullptr); EXPECT_TRUE(object_c) << "Failed to call the ctor."; clang_destruct(object_c, make_scope(Decls[1], I), true); // Clean up resources @@ -1874,7 +1874,7 @@ TEST(FunctionReflectionTest, Construct) { testing::internal::CaptureStdout(); auto* I = clang_createInterpreterFromRawPtr(Cpp::GetInterpreter()); auto scope_c = make_scope(static_cast(scope), I); - auto object_c = clang_construct(scope_c, nullptr); + auto object_c = clang_construct(scope_c, nullptr, 0UL); EXPECT_TRUE(object_c != nullptr); output = testing::internal::GetCapturedStdout(); EXPECT_EQ(output, "Constructor Executed"); @@ -1950,6 +1950,58 @@ TEST(FunctionReflectionTest, ConstructNested) { output.clear(); } +TEST(FunctionReflectionTest, ConstructArray) { +#if defined(EMSCRIPTEN) && (CLANG_VERSION_MAJOR < 20) + GTEST_SKIP() << "Test fails for LLVM < 20 Emscripten builds"; +#endif + if (llvm::sys::RunningOnValgrind()) + GTEST_SKIP() << "XFAIL due to Valgrind report"; +#ifdef _WIN32 + GTEST_SKIP() << "Disabled on Windows. Needs fixing."; +#endif + + Cpp::CreateInterpreter(); + + Interp->declare(R"( + #include + extern "C" int printf(const char*,...); + class C { + int x; + C() { + x = 42; + printf("\nConstructor Executed\n"); + } + }; + )"); + + Cpp::TCppScope_t scope = Cpp::GetNamed("C"); + std::string output; + + size_t a = 5; // Construct an array of 5 objects + void* where = Cpp::Allocate(scope, a); // operator new + + testing::internal::CaptureStdout(); + EXPECT_TRUE(where == Cpp::Construct(scope, where, a)); // placement new + // Check for the value of x which should be at the start of the object. + EXPECT_TRUE(*(int*)where == 42); + // Check for the value of x in the second object + int* obj = reinterpret_cast(reinterpret_cast(where) + + Cpp::SizeOf(scope)); + EXPECT_TRUE(*obj == 42); + + // Check for the value of x in the last object + obj = reinterpret_cast(reinterpret_cast(where) + + Cpp::SizeOf(scope) * 4); + EXPECT_TRUE(*obj == 42); + Cpp::Destruct(where, scope, /*withFree=*/false, 5); + Cpp::Deallocate(scope, where, 5); + output = testing::internal::GetCapturedStdout(); + EXPECT_EQ(output, + "\nConstructor Executed\n\nConstructor Executed\n\nConstructor " + "Executed\n\nConstructor Executed\n\nConstructor Executed\n"); + output.clear(); +} + TEST(FunctionReflectionTest, Destruct) { #ifdef EMSCRIPTEN GTEST_SKIP() << "Test fails for Emscipten builds"; @@ -1997,7 +2049,7 @@ TEST(FunctionReflectionTest, Destruct) { testing::internal::CaptureStdout(); auto* I = clang_createInterpreterFromRawPtr(Cpp::GetInterpreter()); auto scope_c = make_scope(static_cast(scope), I); - auto object_c = clang_construct(scope_c, nullptr); + auto object_c = clang_construct(scope_c, nullptr, 0UL); clang_destruct(object_c, scope_c, true); output = testing::internal::GetCapturedStdout(); EXPECT_EQ(output, "Destructor Executed"); @@ -2007,6 +2059,81 @@ TEST(FunctionReflectionTest, Destruct) { clang_Interpreter_dispose(I); } +TEST(FunctionReflectionTest, DestructArray) { +#ifdef EMSCRIPTEN + GTEST_SKIP() << "Test fails for Emscipten builds"; +#endif + if (llvm::sys::RunningOnValgrind()) + GTEST_SKIP() << "XFAIL due to Valgrind report"; + +#ifdef _WIN32 + GTEST_SKIP() << "Disabled on Windows. Needs fixing."; +#endif + + std::vector interpreter_args = {"-include", "new"}; + Cpp::CreateInterpreter(interpreter_args); + + Interp->declare(R"( + #include + extern "C" int printf(const char*,...); + class C { + int x; + C() { + x = 42; + } + ~C() { + printf("\nDestructor Executed\n"); + } + }; + )"); + + Cpp::TCppScope_t scope = Cpp::GetNamed("C"); + std::string output; + + size_t a = 5; // Construct an array of 5 objects + void* where = Cpp::Allocate(scope, a); // operator new + EXPECT_TRUE(where == Cpp::Construct(scope, where, a)); // placement new + + // verify the array of objects has been constructed + int* obj = reinterpret_cast(reinterpret_cast(where) + + Cpp::SizeOf(scope) * 4); + EXPECT_TRUE(*obj == 42); + + testing::internal::CaptureStdout(); + // destruct 3 out of 5 objects + Cpp::Destruct(where, scope, false, 3); + output = testing::internal::GetCapturedStdout(); + + EXPECT_EQ( + output, + "\nDestructor Executed\n\nDestructor Executed\n\nDestructor Executed\n"); + output.clear(); + testing::internal::CaptureStdout(); + + // destruct the rest + auto new_head = reinterpret_cast(reinterpret_cast(where) + + Cpp::SizeOf(scope) * 3); + Cpp::Destruct(new_head, scope, false, 2); + + output = testing::internal::GetCapturedStdout(); + EXPECT_EQ(output, "\nDestructor Executed\n\nDestructor Executed\n"); + output.clear(); + + // deallocate since we call the destructor withFree = false + Cpp::Deallocate(scope, where, 5); + + // perform the same withFree=true + where = Cpp::Allocate(scope, a); + EXPECT_TRUE(where == Cpp::Construct(scope, where, a)); + testing::internal::CaptureStdout(); + // FIXME : This should work with the array of objects as well + // Cpp::Destruct(where, scope, true, 5); + Cpp::Destruct(where, scope, true); + output = testing::internal::GetCapturedStdout(); + EXPECT_EQ(output, "\nDestructor Executed\n"); + output.clear(); +} + TEST(FunctionReflectionTest, UndoTest) { #ifdef _WIN32 GTEST_SKIP() << "Disabled on Windows. Needs fixing.";