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.
This commit is contained in:
Andrew Kaster 2024-11-16 14:56:27 -07:00 committed by Andrew Kaster
parent 726f2cfb11
commit 829391e714
Notes: github-actions[bot] 2024-11-19 21:54:22 +00:00
11 changed files with 281 additions and 0 deletions

View file

@ -3,6 +3,7 @@ set(SOURCES
Cell.cpp
CellAllocator.cpp
ConservativeVector.cpp
ForeignCell.cpp
Root.cpp
Heap.cpp
HeapBlock.cpp

View file

@ -0,0 +1,61 @@
/*
* Copyright (c) 2024, Andrew Kaster <andrew@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibGC/DeferGC.h>
#include <LibGC/ForeignCell.h>
#include <LibGC/Heap.h>
namespace GC {
void* ForeignCell::foreign_data()
{
// !!!
auto offset = round_up_to_power_of_two(sizeof(ForeignCell), m_vtable.alignment);
return static_cast<void*>(reinterpret_cast<u8*>(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> 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);
}
}

View file

@ -0,0 +1,180 @@
/*
* Copyright (c) 2024, Andrew Kaster <andrew@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/String.h>
#include <AK/Swift.h>
#include <AK/TypeCasts.h>
#include <LibGC/Cell.h>
#include <LibGC/DeferGC.h>
namespace GC {
template<typename T>
struct ForeignRef;
template<typename T>
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<Cell>);
void (*destroy)(void* thiz, void* clazz);
void (*finalize)(void* thiz, void* clazz);
void (*visit_edges)(void* thiz, void* clazz, Cell::Visitor&);
};
static Ref<ForeignCell> 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<typename T>
struct ForeignRef {
friend struct ForeignPtr<T>;
template<typename... Args>
static ForeignRef allocate(Heap& heap, Args... args)
{
DeferGC const defer_gc(heap);
auto* cell = T::create(&heap, forward<Args>(args)...);
if constexpr (IsSame<decltype(cell), Cell*>) {
return ForeignRef(*verify_cast<ForeignCell>(cell));
} else {
static_assert(IsSame<decltype(cell), void*>);
auto* cast_cell = static_cast<Cell*>(cell);
return ForeignRef(*verify_cast<ForeignCell>(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<T*>(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<ForeignCell> cell() const { return m_cell; }
void visit_edges(Cell::Visitor& visitor)
{
visitor.visit(m_cell);
}
private:
Ref<ForeignCell> m_cell;
T* m_data { nullptr };
};
template<typename T>
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<T*>(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<T*>(m_cell->foreign_data()) : nullptr;
}
ForeignPtr(ForeignRef<T> 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<ForeignCell> cell() const { return m_cell; }
void visit_edges(Cell::Visitor& visitor)
{
visitor.visit(m_cell);
}
private:
Ptr<ForeignCell> m_cell;
T* m_data { nullptr };
};
}

View file

@ -13,6 +13,7 @@ namespace GC {
class Cell;
class CellAllocator;
class DeferGC;
class ForeignCell;
class RootImpl;
class Heap;
class HeapBlock;

View file

@ -79,6 +79,7 @@ private:
friend class MarkingVisitor;
friend class GraphConstructorVisitor;
friend class DeferGC;
friend class ForeignCell;
void defer_gc();
void undefer_gc();

View file

@ -314,6 +314,9 @@ static std::optional<CellTypeWithOrigin> 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<CellTypeWithOrigin> 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<LibJSCellMacro::Type> 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 },

View file

@ -13,6 +13,7 @@
struct LibJSCellMacro {
enum class Type {
GCCell,
ForeignCell,
JSObject,
JSEnvironment,
JSPrototypeObject,

View file

@ -6,6 +6,7 @@
// RUN: %clang++ -cc1 -verify %plugin_opts% %s 2>&1
#include <LibGC/ForeignCell.h>
#include <LibJS/Runtime/PrototypeObject.h>
#include <LibWeb/Bindings/PlatformObject.h>
@ -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 {
};

View file

@ -7,6 +7,7 @@
// RUN: %clang++ -cc1 -verify %plugin_opts% %s 2>&1
// expected-no-diagnostics
#include <LibGC/ForeignCell.h>
#include <LibJS/Runtime/PrototypeObject.h>
#include <LibWeb/Bindings/PlatformObject.h>
@ -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);
};

View file

@ -6,6 +6,7 @@
// RUN: %clang++ -cc1 -verify %plugin_opts% %s 2>&1
#include <LibGC/ForeignCell.h>
#include <LibJS/Runtime/Object.h>
#include <LibJS/Runtime/PrototypeObject.h>
@ -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 {

View file

@ -6,6 +6,7 @@
// RUN: %clang++ -cc1 -verify %plugin_opts% %s 2>&1
#include <LibGC/ForeignCell.h>
#include <LibJS/Runtime/PrototypeObject.h>
#include <LibWeb/Bindings/PlatformObject.h>
@ -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);