From 829391e7144d167b7db32f61bd9b4425e52c02d9 Mon Sep 17 00:00:00 2001 From: Andrew Kaster Date: Sat, 16 Nov 2024 14:56:27 -0700 Subject: [PATCH] LibGC: Add a ForeignCell class for ownership of non-C++ objects This will allow us to use the GC to manage the lifetime of objects that are not C++ objects, such as Swift objects. In the future we could expand this cursed FFI to other languages as well. --- Libraries/LibGC/CMakeLists.txt | 1 + Libraries/LibGC/ForeignCell.cpp | 61 ++++++ Libraries/LibGC/ForeignCell.h | 180 ++++++++++++++++++ Libraries/LibGC/Forward.h | 1 + Libraries/LibGC/Heap.h | 1 + .../ClangPlugins/LibJSGCPluginAction.cpp | 9 + Meta/Lagom/ClangPlugins/LibJSGCPluginAction.h | 1 + .../classes_are_missing_expected_macros.cpp | 5 + .../Macros/classes_have_expected_macros.cpp | 5 + .../classes_have_incorrect_macro_types.cpp | 11 ++ .../Macros/wrong_classname_arg.cpp | 6 + 11 files changed, 281 insertions(+) create mode 100644 Libraries/LibGC/ForeignCell.cpp create mode 100644 Libraries/LibGC/ForeignCell.h diff --git a/Libraries/LibGC/CMakeLists.txt b/Libraries/LibGC/CMakeLists.txt index be7c1d07aa7..491a49204b4 100644 --- a/Libraries/LibGC/CMakeLists.txt +++ b/Libraries/LibGC/CMakeLists.txt @@ -3,6 +3,7 @@ set(SOURCES Cell.cpp CellAllocator.cpp ConservativeVector.cpp + ForeignCell.cpp Root.cpp Heap.cpp HeapBlock.cpp diff --git a/Libraries/LibGC/ForeignCell.cpp b/Libraries/LibGC/ForeignCell.cpp new file mode 100644 index 00000000000..0d6539433fb --- /dev/null +++ b/Libraries/LibGC/ForeignCell.cpp @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2024, Andrew Kaster + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include + +namespace GC { + +void* ForeignCell::foreign_data() +{ + // !!! + auto offset = round_up_to_power_of_two(sizeof(ForeignCell), m_vtable.alignment); + return static_cast(reinterpret_cast(this) + offset); +} + +ForeignCell::ForeignCell(ForeignCell::Vtable vtable) + : m_vtable(move(vtable)) +{ + if (m_vtable.initialize) + m_vtable.initialize(foreign_data(), m_vtable.class_metadata_pointer, *this); +} + +ForeignCell::~ForeignCell() +{ + if (m_vtable.destroy) + m_vtable.destroy(foreign_data(), m_vtable.class_metadata_pointer); +} + +Ref ForeignCell::create(Heap& heap, size_t size, ForeignCell::Vtable vtable) +{ + // NOTE: GC must be deferred so that a collection during allocation doesn't get tripped + // up looking for the Cell pointer on the stack or in a register when it might only exist in the heap. + // We can't guarantee that the ForeignCell will be stashed in a proper ForeignRef/ForeignPtr or similar + // foreign type until after all the dust has settled on both sides of the FFI boundary. + VERIFY(heap.is_gc_deferred()); + VERIFY(is_power_of_two(vtable.alignment)); + auto& allocator = heap.allocator_for_size(sizeof(ForeignCell) + round_up_to_power_of_two(size, vtable.alignment)); + auto* memory = allocator.allocate_cell(heap); + auto* foreign_cell = new (memory) ForeignCell(move(vtable)); + return *foreign_cell; +} + +void ForeignCell::finalize() +{ + Base::finalize(); + if (m_vtable.finalize) + m_vtable.finalize(foreign_data(), m_vtable.class_metadata_pointer); +} + +void ForeignCell::visit_edges(Cell::Visitor& visitor) +{ + Base::visit_edges(visitor); + if (m_vtable.visit_edges) + m_vtable.visit_edges(foreign_data(), m_vtable.class_metadata_pointer, visitor); +} + +} diff --git a/Libraries/LibGC/ForeignCell.h b/Libraries/LibGC/ForeignCell.h new file mode 100644 index 00000000000..4edf9203acd --- /dev/null +++ b/Libraries/LibGC/ForeignCell.h @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2024, Andrew Kaster + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace GC { + +template +struct ForeignRef; + +template +struct ForeignPtr; + +#define FOREIGN_CELL(class_, base_class) \ + using Base = base_class; \ + friend class GC::Heap; + +class ForeignCell : public Cell { + FOREIGN_CELL(ForeignCell, Cell); + +public: + struct Vtable { + // Holds a pointer to the foreign vtable information such as + // a jclass in Java, or a Swift type metadata pointer + void* class_metadata_pointer = nullptr; + + // FIXME: FlyString? The class name must be owned by the ForeignCell so it can vend StringViews + // We should properly cache the name and class info pointer to avoid string churn + String class_name; + + size_t alignment { 1 }; + + void (*initialize)(void* thiz, void* clazz, Ref); + void (*destroy)(void* thiz, void* clazz); + void (*finalize)(void* thiz, void* clazz); + void (*visit_edges)(void* thiz, void* clazz, Cell::Visitor&); + }; + static Ref create(Heap&, size_t size, Vtable); + + void* foreign_data() SWIFT_RETURNS_INDEPENDENT_VALUE; // technically lying to swift, but it's fiiiiine + + // ^Cell + virtual void finalize() override; + virtual void visit_edges(Cell::Visitor& visitor) override; + virtual StringView class_name() const override { return m_vtable.class_name; } + + ~ForeignCell(); + +private: + ForeignCell(Vtable vtable); + + Vtable m_vtable; +} SWIFT_IMMORTAL_REFERENCE; + +template +struct ForeignRef { + friend struct ForeignPtr; + + template + static ForeignRef allocate(Heap& heap, Args... args) + { + DeferGC const defer_gc(heap); + auto* cell = T::create(&heap, forward(args)...); + if constexpr (IsSame) { + return ForeignRef(*verify_cast(cell)); + } else { + static_assert(IsSame); + auto* cast_cell = static_cast(cell); + return ForeignRef(*verify_cast(cast_cell)); + } + } + + ForeignRef() = delete; + + // This constructor should only be called directly after allocating a foreign cell by calling an FFI create method + ForeignRef(ForeignCell& cell) + : m_cell(cell) + { + // FIXME: This is super dangerous. How can we assert that the cell is actually a T? + m_data = static_cast(m_cell->foreign_data()); + } + + ~ForeignRef() = default; + ForeignRef(ForeignRef const& other) = default; + ForeignRef& operator=(ForeignRef const& other) = default; + + RETURNS_NONNULL T* operator->() const { return m_data; } + [[nodiscard]] T& operator*() const { return *m_data; } + + RETURNS_NONNULL T* ptr() const { return m_data; } + RETURNS_NONNULL operator T*() const { return m_data; } + + operator T&() const { return *m_data; } + + Ref cell() const { return m_cell; } + + void visit_edges(Cell::Visitor& visitor) + { + visitor.visit(m_cell); + } + +private: + Ref m_cell; + T* m_data { nullptr }; +}; + +template +struct ForeignPtr { + constexpr ForeignPtr() = default; + + // This constructor should only be called directly after allocating a foreign cell by calling an FFI create method + ForeignPtr(ForeignCell& cell) + : m_cell(&cell) + { + // FIXME: This is super dangerous. How can we assert that the cell is actually a T? + m_data = static_cast(m_cell->foreign_data()); + } + + // This constructor should only be called directly after allocating a foreign cell by calling an FFI create method + ForeignPtr(ForeignCell* cell) + : m_cell(cell) + { + // FIXME: This is super dangerous. How can we assert that the cell is actually a T? + m_data = m_cell ? static_cast(m_cell->foreign_data()) : nullptr; + } + + ForeignPtr(ForeignRef const& other) + : m_cell(other.m_cell) + , m_data(other.m_data) + { + } + + ForeignPtr(nullptr_t) + : m_cell(nullptr) + { + } + + ForeignPtr(ForeignPtr const& other) = default; + ForeignPtr& operator=(ForeignPtr const& other) = default; + + T* operator->() const + { + ASSERT(m_cell && m_data); + return m_data; + } + + [[nodiscard]] T& operator*() const + { + ASSERT(m_cell && m_data); + return *m_data; + } + + operator T*() const { return m_data; } + T* ptr() const { return m_data; } + + explicit operator bool() const { return !!m_cell; } + bool operator!() const { return !m_cell; } + + Ptr cell() const { return m_cell; } + + void visit_edges(Cell::Visitor& visitor) + { + visitor.visit(m_cell); + } + +private: + Ptr m_cell; + T* m_data { nullptr }; +}; + +} diff --git a/Libraries/LibGC/Forward.h b/Libraries/LibGC/Forward.h index 56c28e3589c..8b33eb57523 100644 --- a/Libraries/LibGC/Forward.h +++ b/Libraries/LibGC/Forward.h @@ -13,6 +13,7 @@ namespace GC { class Cell; class CellAllocator; class DeferGC; +class ForeignCell; class RootImpl; class Heap; class HeapBlock; diff --git a/Libraries/LibGC/Heap.h b/Libraries/LibGC/Heap.h index ecadf745ad7..7a2b1fed27a 100644 --- a/Libraries/LibGC/Heap.h +++ b/Libraries/LibGC/Heap.h @@ -79,6 +79,7 @@ private: friend class MarkingVisitor; friend class GraphConstructorVisitor; friend class DeferGC; + friend class ForeignCell; void defer_gc(); void undefer_gc(); diff --git a/Meta/Lagom/ClangPlugins/LibJSGCPluginAction.cpp b/Meta/Lagom/ClangPlugins/LibJSGCPluginAction.cpp index 61a7f4a3d3e..8047d4bb3f4 100644 --- a/Meta/Lagom/ClangPlugins/LibJSGCPluginAction.cpp +++ b/Meta/Lagom/ClangPlugins/LibJSGCPluginAction.cpp @@ -314,6 +314,9 @@ static std::optional find_cell_type_with_origin(clang::CXXRe if (base_name == "GC::Cell") return CellTypeWithOrigin { *base_record, LibJSCellMacro::Type::GCCell }; + if (base_name == "GC::ForeignCell") + return CellTypeWithOrigin { *base_record, LibJSCellMacro::Type::ForeignCell }; + if (base_name == "JS::Object") return CellTypeWithOrigin { *base_record, LibJSCellMacro::Type::JSObject }; @@ -336,6 +339,9 @@ static std::optional find_cell_type_with_origin(clang::CXXRe LibJSGCVisitor::CellMacroExpectation LibJSGCVisitor::get_record_cell_macro_expectation(clang::CXXRecordDecl const& record) { + if (record.getQualifiedNameAsString() == "GC::ForeignCell") + return { LibJSCellMacro::Type::ForeignCell, "Cell" }; + auto origin = find_cell_type_with_origin(record); assert(origin.has_value()); @@ -471,6 +477,8 @@ char const* LibJSCellMacro::type_name(Type type) switch (type) { case Type::GCCell: return "GC_CELL"; + case Type::ForeignCell: + return "FOREIGN_CELL"; case Type::JSObject: return "JS_OBJECT"; case Type::JSEnvironment: @@ -499,6 +507,7 @@ void LibJSPPCallbacks::MacroExpands(clang::Token const& name_token, clang::Macro if (auto* ident_info = name_token.getIdentifierInfo()) { static llvm::StringMap libjs_macro_types { { "GC_CELL", LibJSCellMacro::Type::GCCell }, + { "FOREIGN_CELL", LibJSCellMacro::Type::ForeignCell }, { "JS_OBJECT", LibJSCellMacro::Type::JSObject }, { "JS_ENVIRONMENT", LibJSCellMacro::Type::JSEnvironment }, { "JS_PROTOTYPE_OBJECT", LibJSCellMacro::Type::JSPrototypeObject }, diff --git a/Meta/Lagom/ClangPlugins/LibJSGCPluginAction.h b/Meta/Lagom/ClangPlugins/LibJSGCPluginAction.h index 667dc8c810d..0a45e5111a2 100644 --- a/Meta/Lagom/ClangPlugins/LibJSGCPluginAction.h +++ b/Meta/Lagom/ClangPlugins/LibJSGCPluginAction.h @@ -13,6 +13,7 @@ struct LibJSCellMacro { enum class Type { GCCell, + ForeignCell, JSObject, JSEnvironment, JSPrototypeObject, diff --git a/Tests/ClangPlugins/LibJSGCTests/Macros/classes_are_missing_expected_macros.cpp b/Tests/ClangPlugins/LibJSGCTests/Macros/classes_are_missing_expected_macros.cpp index 8933653b7a9..59a3159d2c3 100644 --- a/Tests/ClangPlugins/LibJSGCTests/Macros/classes_are_missing_expected_macros.cpp +++ b/Tests/ClangPlugins/LibJSGCTests/Macros/classes_are_missing_expected_macros.cpp @@ -6,6 +6,7 @@ // RUN: %clang++ -cc1 -verify %plugin_opts% %s 2>&1 +#include #include #include @@ -13,6 +14,10 @@ class TestCellClass : JS::Cell { }; +// expected-error@+1 {{Expected record to have a FOREIGN_CELL macro invocation}} +class TestForeignCellClass : GC::ForeignCell { +}; + // expected-error@+1 {{Expected record to have a JS_OBJECT macro invocation}} class TestObjectClass : JS::Object { }; diff --git a/Tests/ClangPlugins/LibJSGCTests/Macros/classes_have_expected_macros.cpp b/Tests/ClangPlugins/LibJSGCTests/Macros/classes_have_expected_macros.cpp index a4759fcb666..20c5484f61d 100644 --- a/Tests/ClangPlugins/LibJSGCTests/Macros/classes_have_expected_macros.cpp +++ b/Tests/ClangPlugins/LibJSGCTests/Macros/classes_have_expected_macros.cpp @@ -7,6 +7,7 @@ // RUN: %clang++ -cc1 -verify %plugin_opts% %s 2>&1 // expected-no-diagnostics +#include #include #include @@ -14,6 +15,10 @@ class TestCellClass : JS::Cell { GC_CELL(TestCellClass, JS::Cell); }; +class TestForeignCellClass : GC::ForeignCell { + FOREIGN_CELL(TestForeignCellClass, GC::ForeignCell); +}; + class TestObjectClass : JS::Object { JS_OBJECT(TestObjectClass, JS::Object); }; diff --git a/Tests/ClangPlugins/LibJSGCTests/Macros/classes_have_incorrect_macro_types.cpp b/Tests/ClangPlugins/LibJSGCTests/Macros/classes_have_incorrect_macro_types.cpp index c9322f86c71..7cf8d1337c8 100644 --- a/Tests/ClangPlugins/LibJSGCTests/Macros/classes_have_incorrect_macro_types.cpp +++ b/Tests/ClangPlugins/LibJSGCTests/Macros/classes_have_incorrect_macro_types.cpp @@ -6,6 +6,7 @@ // RUN: %clang++ -cc1 -verify %plugin_opts% %s 2>&1 +#include #include #include @@ -34,6 +35,16 @@ class ObjectWithEnvironmentMacro : JS::Object { JS_ENVIRONMENT(ObjectWithEnvironmentMacro, JS::Object); }; +class CellWithForeignCellMacro : GC::Cell { + // expected-error@+1 {{Invalid GC-CELL-like macro invocation; expected GC_CELL}} + FOREIGN_CELL(CellWithForeignCellMacro, GC::Cell); +}; + +class ObjectWithForeignCellMacro : JS::Object { + // expected-error@+1 {{Invalid GC-CELL-like macro invocation; expected JS_OBJECT}} + FOREIGN_CELL(ObjectWithForeignCellMacro, JS::Object); +}; + // JS_PROTOTYPE_OBJECT can only be used in the JS namespace namespace JS { diff --git a/Tests/ClangPlugins/LibJSGCTests/Macros/wrong_classname_arg.cpp b/Tests/ClangPlugins/LibJSGCTests/Macros/wrong_classname_arg.cpp index 9f5e9f7aca0..c5ab6501a7a 100644 --- a/Tests/ClangPlugins/LibJSGCTests/Macros/wrong_classname_arg.cpp +++ b/Tests/ClangPlugins/LibJSGCTests/Macros/wrong_classname_arg.cpp @@ -6,6 +6,7 @@ // RUN: %clang++ -cc1 -verify %plugin_opts% %s 2>&1 +#include #include #include @@ -16,6 +17,11 @@ class TestCellClass : JS::Cell { GC_CELL(bad, JS::Cell); }; +class TestForeignCellClass : GC::ForeignCell { + // expected-error@+1 {{Expected first argument of FOREIGN_CELL macro invocation to be TestForeignCellClass}} + FOREIGN_CELL(bad, GC::ForeignCell); +}; + class TestObjectClass : JS::Object { // expected-error@+1 {{Expected first argument of JS_OBJECT macro invocation to be TestObjectClass}} JS_OBJECT(bad, JS::Object);