LibGC: Add GC::Weak<T> as an alternative to AK::WeakPtr<T>

This is a weak pointer that integrates with the garbage collector.
It has a number of differences compared to AK::WeakPtr, including:

- The "control block" is allocated from a well-packed WeakBlock owned by
  the GC heap, not just a generic malloc allocation.

- Pointers to dead cells are nulled out by the garbage collector
  immediately before running destructors.

- It works on any GC::Cell derived type, meaning you don't have to
  inherit from AK::Weakable for the ability to be weakly referenced.

- The Weak always points to a control block, even when "null" (it then
  points to a null WeakImpl), which means one less null check when
  chasing pointers.
This commit is contained in:
Andreas Kling
2025-10-16 11:10:01 +02:00
committed by Andreas Kling
parent 127208f3d6
commit 25a5ed94d6
9 changed files with 449 additions and 1 deletions

View File

@@ -276,11 +276,13 @@
# define ASAN_UNPOISON_MEMORY_REGION(addr, size) __asan_unpoison_memory_region(addr, size)
# define LSAN_REGISTER_ROOT_REGION(base, size) __lsan_register_root_region(base, size)
# define LSAN_UNREGISTER_ROOT_REGION(base, size) __lsan_unregister_root_region(base, size)
# define LSAN_IGNORE_OBJECT(base) __lsan_ignore_object(base)
#else
# define ASAN_POISON_MEMORY_REGION(addr, size)
# define ASAN_UNPOISON_MEMORY_REGION(addr, size)
# define LSAN_REGISTER_ROOT_REGION(base, size)
# define LSAN_UNREGISTER_ROOT_REGION(base, size)
# define LSAN_IGNORE_OBJECT(base)
#endif
#if __has_feature(blocks)

View File

@@ -9,6 +9,7 @@ set(SOURCES
RootVector.cpp
Heap.cpp
HeapBlock.cpp
WeakBlock.cpp
WeakContainer.cpp
)

View File

@@ -20,6 +20,7 @@ class Heap;
class HeapBlock;
class NanBoxedValue;
class WeakContainer;
class WeakImpl;
template<typename T>
class Function;

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2020-2022, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2020-2025, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2023, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
@@ -20,6 +20,8 @@
#include <LibGC/HeapBlock.h>
#include <LibGC/NanBoxedValue.h>
#include <LibGC/Root.h>
#include <LibGC/Weak.h>
#include <LibGC/WeakInlines.h>
#include <setjmp.h>
#ifdef HAS_ADDRESS_SANITIZER
@@ -258,6 +260,7 @@ void Heap::collect_garbage(CollectionType collection_type, bool print_report)
mark_live_cells(roots);
}
finalize_unmarked_cells();
sweep_weak_blocks();
sweep_dead_cells(print_report, collection_measurement_timer);
}
@@ -462,6 +465,22 @@ void Heap::finalize_unmarked_cells()
});
}
void Heap::sweep_weak_blocks()
{
for (auto& weak_block : m_usable_weak_blocks) {
weak_block.sweep();
}
Vector<WeakBlock&> now_usable_weak_blocks;
for (auto& weak_block : m_full_weak_blocks) {
weak_block.sweep();
if (weak_block.can_allocate())
now_usable_weak_blocks.append(weak_block);
}
for (auto& weak_block : now_usable_weak_blocks) {
m_usable_weak_blocks.append(weak_block);
}
}
void Heap::sweep_dead_cells(bool print_report, Core::ElapsedTimer const& measurement_timer)
{
dbgln_if(HEAP_DEBUG, "sweep_dead_cells:");
@@ -559,4 +578,21 @@ void Heap::uproot_cell(Cell* cell)
m_uprooted_cells.append(cell);
}
WeakImpl* Heap::create_weak_impl(void* ptr)
{
if (m_usable_weak_blocks.is_empty()) {
// NOTE: These are leaked on Heap destruction, but that's fine since Heap is tied to process lifetime.
auto* weak_block = WeakBlock::create();
m_usable_weak_blocks.append(*weak_block);
}
auto* weak_block = m_usable_weak_blocks.first();
auto* new_weak_impl = weak_block->allocate(static_cast<Cell*>(ptr));
if (!weak_block->can_allocate()) {
m_full_weak_blocks.append(*weak_block);
}
return new_weak_impl;
}
}

View File

@@ -25,6 +25,7 @@
#include <LibGC/Root.h>
#include <LibGC/RootHashMap.h>
#include <LibGC/RootVector.h>
#include <LibGC/WeakBlock.h>
#include <LibGC/WeakContainer.h>
namespace GC {
@@ -81,6 +82,8 @@ public:
void enqueue_post_gc_task(AK::Function<void()>);
WeakImpl* create_weak_impl(void*);
private:
friend class MarkingVisitor;
friend class GraphConstructorVisitor;
@@ -113,6 +116,7 @@ private:
void mark_live_cells(HashMap<Cell*, HeapRoot> const& live_cells);
void finalize_unmarked_cells();
void sweep_dead_cells(bool print_report, Core::ElapsedTimer const&);
void sweep_weak_blocks();
ALWAYS_INLINE CellAllocator& allocator_for_size(size_t cell_size)
{
@@ -159,6 +163,9 @@ private:
AK::Function<void(HashMap<Cell*, GC::HeapRoot>&)> m_gather_embedder_roots;
Vector<AK::Function<void()>> m_post_gc_tasks;
WeakBlock::List m_usable_weak_blocks;
WeakBlock::List m_full_weak_blocks;
} SWIFT_IMMORTAL_REFERENCE;
inline void Heap::did_create_root(Badge<RootImpl>, RootImpl& impl)

166
Libraries/LibGC/Weak.h Normal file
View File

@@ -0,0 +1,166 @@
/*
* Copyright (c) 2025, Andreas Kling <andreas@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Badge.h>
#include <AK/RefPtr.h>
#include <LibGC/Export.h>
#include <LibGC/Ptr.h>
namespace GC {
class WeakBlock;
class WeakImpl {
public:
// NOTE: Null GC::Weaks point at this WeakImpl. This allows Weak to always chase the impl pointer without null-checking it.
static GC_API WeakImpl the_null_weak_impl;
WeakImpl() = default;
WeakImpl(void* ptr)
: m_ptr(ptr)
{
}
void* ptr() const { return m_ptr; }
void set_ptr(Badge<WeakBlock>, void* ptr) { m_ptr = ptr; }
bool operator==(WeakImpl const& other) const { return m_ptr == other.m_ptr; }
bool operator!=(WeakImpl const& other) const { return m_ptr != other.m_ptr; }
void ref() const { ++m_ref_count; }
void unref() const
{
VERIFY(m_ref_count);
--m_ref_count;
}
size_t ref_count() const { return m_ref_count; }
enum class State {
Allocated,
Freelist,
};
void set_state(State state) { m_state = state; }
State state() const { return m_state; }
private:
mutable size_t m_ref_count { 0 };
State m_state { State::Allocated };
void* m_ptr { nullptr };
};
template<typename T>
class Weak {
public:
constexpr Weak() = default;
Weak(nullptr_t) { }
Weak(T const* ptr);
Weak(T const& ptr);
template<typename U>
Weak(Weak<U> const& other)
requires(IsConvertible<U*, T*>);
Weak(Ref<T> const& other);
template<typename U>
Weak(Ref<U> const& other)
requires(IsConvertible<U*, T*>);
template<typename U>
Weak& operator=(Weak<U> const& other)
requires(IsConvertible<U*, T*>)
{
m_impl = other.impl();
return *this;
}
Weak& operator=(Ref<T> const& other);
template<typename U>
Weak& operator=(Ref<U> const& other)
requires(IsConvertible<U*, T*>);
Weak& operator=(T const& other);
template<typename U>
Weak& operator=(U const& other)
requires(IsConvertible<U*, T*>);
Weak& operator=(T const* other);
template<typename U>
Weak& operator=(U const* other)
requires(IsConvertible<U*, T*>);
T* operator->() const
{
ASSERT(ptr());
return ptr();
}
[[nodiscard]] T& operator*() const
{
ASSERT(ptr());
return *ptr();
}
Ptr<T> ptr() const { return static_cast<T*>(impl().ptr()); }
explicit operator bool() const { return !!ptr(); }
bool operator!() const { return !ptr(); }
operator T*() const { return ptr(); }
Ref<T> as_nonnull() const
{
ASSERT(ptr());
return *ptr();
}
WeakImpl& impl() const { return *m_impl; }
private:
NonnullRefPtr<WeakImpl> m_impl { WeakImpl::the_null_weak_impl };
};
template<typename T, typename U>
inline bool operator==(Weak<T> const& a, Ptr<U> const& b)
{
return a.ptr() == b.ptr();
}
template<typename T, typename U>
inline bool operator==(Weak<T> const& a, Ref<U> const& b)
{
return a.ptr() == b.ptr();
}
}
namespace AK {
template<typename T>
struct Traits<GC::Weak<T>> : public DefaultTraits<GC::Weak<T>> {
static unsigned hash(GC::Weak<T> const& value)
{
return Traits<T*>::hash(value.ptr());
}
};
template<typename T>
struct Formatter<GC::Weak<T>> : Formatter<T const*> {
ErrorOr<void> format(FormatBuilder& builder, GC::Weak<T> const& value)
{
return Formatter<T const*>::format(builder, value.ptr());
}
};
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright (c) 2025, Andreas Kling <andreas@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibGC/Cell.h>
#include <LibGC/WeakBlock.h>
#include <sys/mman.h>
#if defined(AK_OS_WINDOWS)
# include <AK/Windows.h>
# include <memoryapi.h>
#endif
namespace GC {
WeakImpl WeakImpl::the_null_weak_impl;
WeakBlock* WeakBlock::create()
{
#if !defined(AK_OS_WINDOWS)
auto* block = (HeapBlock*)mmap(nullptr, WeakBlock::BLOCK_SIZE, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
VERIFY(block != MAP_FAILED);
#else
auto* block = (HeapBlock*)VirtualAlloc(NULL, WeakBlock::BLOCK_SIZE, MEM_COMMIT, PAGE_READWRITE);
VERIFY(block);
#endif
return new (block) WeakBlock;
}
WeakBlock::WeakBlock()
{
for (size_t i = 0; i < IMPL_COUNT; ++i) {
m_impls[i].set_ptr({}, i + 1 < IMPL_COUNT ? &m_impls[i + 1] : nullptr);
m_impls[i].set_state(WeakImpl::State::Freelist);
}
m_freelist = &m_impls[0];
}
WeakBlock::~WeakBlock() = default;
WeakImpl* WeakBlock::allocate(Cell* cell)
{
auto* impl = m_freelist;
if (!impl)
return nullptr;
VERIFY(impl->ref_count() == 0);
m_freelist = impl->ptr() ? static_cast<WeakImpl*>(impl->ptr()) : nullptr;
impl->set_ptr({}, cell);
impl->set_state(WeakImpl::State::Allocated);
return impl;
}
void WeakBlock::deallocate(WeakImpl* impl)
{
VERIFY(impl->ref_count() == 0);
impl->set_ptr({}, m_freelist);
impl->set_state(WeakImpl::State::Freelist);
m_freelist = impl;
}
void WeakBlock::sweep()
{
for (size_t i = 0; i < IMPL_COUNT; ++i) {
auto& impl = m_impls[i];
if (impl.state() == WeakImpl::State::Freelist)
continue;
auto* cell = static_cast<Cell*>(impl.ptr());
if (!cell || !cell->is_marked())
impl.set_ptr({}, nullptr);
if (impl.ref_count() == 0)
deallocate(&impl);
}
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright (c) 2025, Andreas Kling <andreas@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/IntrusiveList.h>
#include <LibGC/Forward.h>
#include <LibGC/Weak.h>
namespace GC {
class GC_API WeakBlock {
public:
static constexpr size_t BLOCK_SIZE = 16 * KiB;
static WeakBlock* create();
WeakImpl* allocate(Cell*);
void deallocate(WeakImpl*);
bool can_allocate() const { return m_freelist != nullptr; }
void sweep();
private:
WeakBlock();
~WeakBlock();
IntrusiveListNode<WeakBlock> m_list_node;
public:
using List = IntrusiveList<&WeakBlock::m_list_node>;
WeakImpl* m_freelist { nullptr };
static constexpr size_t IMPL_COUNT = (BLOCK_SIZE - sizeof(m_list_node) - sizeof(WeakImpl*)) / sizeof(WeakImpl);
WeakImpl m_impls[IMPL_COUNT];
};
static_assert(sizeof(WeakBlock) <= WeakBlock::BLOCK_SIZE);
}

View File

@@ -0,0 +1,113 @@
/*
* Copyright (c) 2025, Andreas Kling <andreas@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibGC/Weak.h>
namespace GC {
template<typename T>
Weak<T>::Weak(T const* ptr)
: m_impl(ptr ? *ptr->heap().create_weak_impl(const_cast<void*>(static_cast<void const*>(ptr))) : WeakImpl::the_null_weak_impl)
{
}
template<typename T>
Weak<T>::Weak(T const& ptr)
: m_impl(*ptr.heap().create_weak_impl(const_cast<void*>(static_cast<void const*>(&ptr))))
{
}
template<typename T>
template<typename U>
Weak<T>::Weak(Weak<U> const& other)
requires(IsConvertible<U*, T*>)
: m_impl(other.impl())
{
}
template<typename T>
Weak<T>::Weak(Ref<T> const& other)
: m_impl(*other.ptr()->heap().create_weak_impl(other.ptr()))
{
}
template<typename T>
template<typename U>
Weak<T>::Weak(Ref<U> const& other)
requires(IsConvertible<U*, T*>)
: m_impl(*other.ptr()->heap().create_weak_impl(other.ptr()))
{
}
template<typename T>
template<typename U>
Weak<T>& Weak<T>::operator=(U const& other)
requires(IsConvertible<U*, T*>)
{
if (ptr() != other) {
m_impl = *other.heap().create_weak_impl(const_cast<void*>(static_cast<void const*>(&other)));
}
return *this;
}
template<typename T>
Weak<T>& Weak<T>::operator=(Ref<T> const& other)
{
if (ptr() != other.ptr()) {
m_impl = *other.ptr()->heap().create_weak_impl(other.ptr());
}
return *this;
}
template<typename T>
template<typename U>
Weak<T>& Weak<T>::operator=(Ref<U> const& other)
requires(IsConvertible<U*, T*>)
{
if (ptr() != other.ptr()) {
m_impl = *other.ptr()->heap().create_weak_impl(other.ptr());
}
return *this;
}
template<typename T>
Weak<T>& Weak<T>::operator=(T const& other)
{
if (ptr() != &other) {
m_impl = *other.heap().create_weak_impl(const_cast<void*>(static_cast<void const*>(&other)));
}
return *this;
}
template<typename T>
Weak<T>& Weak<T>::operator=(T const* other)
{
if (ptr() != other) {
if (other)
m_impl = *other->heap().create_weak_impl(const_cast<void*>(static_cast<void const*>(other)));
else
m_impl = WeakImpl::the_null_weak_impl;
}
return *this;
}
template<typename T>
template<typename U>
Weak<T>& Weak<T>::operator=(U const* other)
requires(IsConvertible<U*, T*>)
{
if (ptr() != other) {
if (other)
m_impl = *other->heap().create_weak_impl(const_cast<void*>(static_cast<void const*>(other)));
else
m_impl = WeakImpl::the_null_weak_impl;
}
return *this;
}
}