From fca6fd0b859701fdc838b0ef03f0bd54f5c8a4fa Mon Sep 17 00:00:00 2001 From: Andrew Kaster Date: Sat, 16 Nov 2024 14:58:28 -0700 Subject: [PATCH] LibGC: Add Swift bindings to the GC heap This includes a protocol for creating LibGC Heap allocated Swift objects. Pay no attention to the Unmanaged shenanigans, they are all behind the curtain. --- Libraries/LibGC/CMakeLists.txt | 9 +++ Libraries/LibGC/Heap+Swift.swift | 105 +++++++++++++++++++++++++++++++ Libraries/LibGC/Heap.h | 3 +- Meta/Lagom/CMakeLists.txt | 1 + Tests/LibGC/CMakeLists.txt | 17 +++++ Tests/LibGC/TestGCBindings.swift | 39 ++++++++++++ Tests/LibGC/TestHeap.cpp | 15 +++++ Tests/LibGC/TestHeap.h | 11 ++++ Tests/LibGC/TestInterop.cpp | 44 +++++++++++++ Tests/LibGC/TestInterop.h | 9 +++ Tests/LibGC/module.modulemap | 6 ++ 11 files changed, 258 insertions(+), 1 deletion(-) create mode 100644 Libraries/LibGC/Heap+Swift.swift create mode 100644 Tests/LibGC/CMakeLists.txt create mode 100644 Tests/LibGC/TestGCBindings.swift create mode 100644 Tests/LibGC/TestHeap.cpp create mode 100644 Tests/LibGC/TestHeap.h create mode 100644 Tests/LibGC/TestInterop.cpp create mode 100644 Tests/LibGC/TestInterop.h create mode 100644 Tests/LibGC/module.modulemap diff --git a/Libraries/LibGC/CMakeLists.txt b/Libraries/LibGC/CMakeLists.txt index 491a49204b4..7d93203ab70 100644 --- a/Libraries/LibGC/CMakeLists.txt +++ b/Libraries/LibGC/CMakeLists.txt @@ -13,3 +13,12 @@ set(SOURCES serenity_lib(LibGC gc) target_link_libraries(LibGC PRIVATE LibCore) + +if (ENABLE_SWIFT) + generate_clang_module_map(LibGC) + target_sources(LibGC PRIVATE + Heap+Swift.swift + ) + target_link_libraries(LibGC PRIVATE AK) + add_swift_target_properties(LibGC LAGOM_LIBRARIES AK) +endif() diff --git a/Libraries/LibGC/Heap+Swift.swift b/Libraries/LibGC/Heap+Swift.swift new file mode 100644 index 00000000000..41eeff56baf --- /dev/null +++ b/Libraries/LibGC/Heap+Swift.swift @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2024, Andrew Kaster + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +import AK +@_exported import GCCxx + +extension GC.Heap { + public func withDeferredGC(_ body: () throws(E) -> R) throws(E) -> R { + let deferredRAII = GC.DeferGC(self) + _ = deferredRAII + return try body() + } +} + +// FIXME: Cell and Cell::Visitor are not imported properly, so we have to treat them as OpaquePointer +public protocol HeapAllocatable { + static func allocate(on heap: GC.Heap) -> UnsafeMutablePointer + + init(cell: OpaquePointer) + + func finalize() + func visitEdges(_ visitor: OpaquePointer) + + var cell: OpaquePointer { get } +} + +// FIXME: Figure out why other modules can't conform to HeapAllocatable +public struct HeapString: HeapAllocatable { + public var string: Swift.String + + public init(cell: OpaquePointer) { + self.cell = cell + self.string = "" + } + + // FIXME: HeapAllocatable cannot be exposed to C++ yet, so we're off to void* paradise + public static func create(on heap: GC.Heap, string: Swift.String) -> OpaquePointer { + // 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 + precondition(heap.is_gc_deferred()) + let heapString = allocate(on: heap) + heapString.pointee.string = string + return heapString.pointee.cell + } + + public var cell: OpaquePointer +} + +// Here be dragons + +func asTypeMetadataPointer(_ type: Any.Type) -> UnsafeMutableRawPointer { + unsafeBitCast(type, to: UnsafeMutableRawPointer.self) +} + +func asHeapAllocatableType(_ typeMetadata: UnsafeMutableRawPointer) -> any HeapAllocatable.Type { + let typeObject = unsafeBitCast(typeMetadata, to: Any.Type.self) + guard let type = typeObject as? any HeapAllocatable.Type else { + fatalError("Passed foreign class but it wasn't a Swift type!") + } + return type +} + +extension HeapAllocatable { + fileprivate static func initializeFromFFI(at this: UnsafeMutableRawPointer, cell: OpaquePointer) { + this.assumingMemoryBound(to: Self.self).initialize(to: Self.self.init(cell: cell)) + } + + fileprivate static func destroyFromFFI(at this: UnsafeMutableRawPointer) { + this.assumingMemoryBound(to: Self.self).deinitialize(count: 1) + } + + fileprivate static func finalizeFromFFI(at this: UnsafeMutableRawPointer) { + this.assumingMemoryBound(to: Self.self).pointee.finalize() + } + + fileprivate static func visitEdgesFromFFI(at this: UnsafeMutableRawPointer, visitor: OpaquePointer) { + this.assumingMemoryBound(to: Self.self).pointee.visitEdges(visitor) + } + + public static func allocate(on heap: GC.Heap) -> UnsafeMutablePointer { + let vtable = GC.ForeignCell.Vtable( + class_metadata_pointer: asTypeMetadataPointer(Self.self), + class_name: AK.String(swiftString: Swift.String(describing: Self.self)), + alignment: MemoryLayout.alignment, + initialize: { this, typeMetadata, cell in + asHeapAllocatableType(typeMetadata!).initializeFromFFI(at: this!, cell: cell.ptr()) + }, + destroy: { this, typeMetadata in + asHeapAllocatableType(typeMetadata!).destroyFromFFI(at: this!) + }, + finalize: { this, typeMetadata in + asHeapAllocatableType(typeMetadata!).finalizeFromFFI(at: this!) + }, + visit_edges: nil + ) + let cell = GC.ForeignCell.create(heap, MemoryLayout.stride, vtable) + return cell.pointee.foreign_data().assumingMemoryBound(to: Self.self) + } + + public func finalize() {} + public func visitEdges(_ visitor: OpaquePointer) {} +} diff --git a/Libraries/LibGC/Heap.h b/Libraries/LibGC/Heap.h index 7a2b1fed27a..b84362fea23 100644 --- a/Libraries/LibGC/Heap.h +++ b/Libraries/LibGC/Heap.h @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -150,7 +151,7 @@ private: bool m_collecting_garbage { false }; StackInfo m_stack_info; AK::Function&)> m_gather_embedder_roots; -}; +} SWIFT_IMMORTAL_REFERENCE; inline void Heap::did_create_root(Badge, RootImpl& impl) { diff --git a/Meta/Lagom/CMakeLists.txt b/Meta/Lagom/CMakeLists.txt index 8ba04b797d1..9a232a5f1ba 100644 --- a/Meta/Lagom/CMakeLists.txt +++ b/Meta/Lagom/CMakeLists.txt @@ -501,6 +501,7 @@ if (BUILD_TESTING) AK LibCrypto LibCompress + LibGC LibTest LibTextCodec LibThreading diff --git a/Tests/LibGC/CMakeLists.txt b/Tests/LibGC/CMakeLists.txt new file mode 100644 index 00000000000..da0f28f02a2 --- /dev/null +++ b/Tests/LibGC/CMakeLists.txt @@ -0,0 +1,17 @@ +if (ENABLE_SWIFT) + find_package(SwiftTesting REQUIRED) + + add_executable(TestGCSwift + TestGCBindings.swift + TestHeap.cpp + TestInterop.cpp + ) + + # FIXME: Swift doesn't seem to like object libraries for @main + target_sources(TestGCSwift PRIVATE ../Resources/SwiftTestMain.swift) + + set_target_properties(TestGCSwift PROPERTIES SUFFIX .swift-testing) + target_include_directories(TestGCSwift PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(TestGCSwift PRIVATE AK LibGC SwiftTesting::SwiftTesting) + add_test(NAME TestGCSwift COMMAND TestGCSwift) +endif() diff --git a/Tests/LibGC/TestGCBindings.swift b/Tests/LibGC/TestGCBindings.swift new file mode 100644 index 00000000000..36e2fde9224 --- /dev/null +++ b/Tests/LibGC/TestGCBindings.swift @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2024, Andrew Kaster + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +import AK +import GC +import GCTesting +import Testing + +// FIXME: We want a type declared *here* for HeapString, but it gives a compiler warning: +// error: type 'GCString' cannot conform to protocol 'HeapAllocatable' because it has requirements that cannot be satisfied +// Even using the same exact code from LibGC/Heap+Swift.swift +// This is likely because one of the required types for HeapAllocatable is not fully imported from C++ and thus can't +// be re-exported by the GC module. + +@Suite(.serialized) +struct TestGCSwiftBindings { + + @Test func createBoundString() { + let heap = test_gc_heap() + let string = heap.withDeferredGC { + return HeapString.allocate(on: heap) + } + #expect(string.pointee.string == "") + heap.collect_garbage(GC.Heap.CollectionType.CollectGarbage) + + string.pointee.string = "Hello, World!" + heap.collect_garbage(GC.Heap.CollectionType.CollectGarbage) + #expect(string.pointee.string == "Hello, World!") + + heap.collect_garbage(GC.Heap.CollectionType.CollectEverything) + } + + @Test func testInterop() { + test_interop() + } +} diff --git a/Tests/LibGC/TestHeap.cpp b/Tests/LibGC/TestHeap.cpp new file mode 100644 index 00000000000..661fbdbc84a --- /dev/null +++ b/Tests/LibGC/TestHeap.cpp @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2024, Andrew Kaster + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "TestHeap.h" +#include + +GC::Heap& test_gc_heap() +{ + // FIXME: The GC heap should become thread aware! + thread_local GC::Heap heap(nullptr, [](auto&) {}); + return heap; +} diff --git a/Tests/LibGC/TestHeap.h b/Tests/LibGC/TestHeap.h new file mode 100644 index 00000000000..32f8adeab2c --- /dev/null +++ b/Tests/LibGC/TestHeap.h @@ -0,0 +1,11 @@ +/* + * Copyright (c) 2024, Andrew Kaster + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +GC::Heap& test_gc_heap(); diff --git a/Tests/LibGC/TestInterop.cpp b/Tests/LibGC/TestInterop.cpp new file mode 100644 index 00000000000..f47f85e9f5b --- /dev/null +++ b/Tests/LibGC/TestInterop.cpp @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2024, Andrew Kaster + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "TestInterop.h" +#include "TestHeap.h" +#include +#include +#include +#include + +#define COLLECT heap.collect_garbage(GC::Heap::CollectionType::CollectGarbage) +#define COLLECT_ALL heap.collect_garbage(GC::Heap::CollectionType::CollectEverything) + +void test_interop() +{ + auto& heap = test_gc_heap(); + + COLLECT_ALL; + + auto string = GC::ForeignRef::allocate(heap, "Hello, World!"); + + COLLECT; + + auto strings_string = std::string(string->getString()); + VERIFY(strings_string == "Hello, World!"); + + COLLECT; + + auto* cell = string->getCell(); + VERIFY(cell == static_cast(string.cell())); + + COLLECT; + + strings_string = std::string(string->getString()); + + COLLECT; + + VERIFY(strings_string == "Hello, World!"); + + COLLECT_ALL; +} diff --git a/Tests/LibGC/TestInterop.h b/Tests/LibGC/TestInterop.h new file mode 100644 index 00000000000..79af1ec90aa --- /dev/null +++ b/Tests/LibGC/TestInterop.h @@ -0,0 +1,9 @@ +/* + * Copyright (c) 2024, Andrew Kaster + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +void test_interop(); diff --git a/Tests/LibGC/module.modulemap b/Tests/LibGC/module.modulemap new file mode 100644 index 00000000000..7ce75c11409 --- /dev/null +++ b/Tests/LibGC/module.modulemap @@ -0,0 +1,6 @@ +module GCTesting { + header "TestHeap.h" + header "TestInterop.h" + requires cplusplus + export * +}