From 732448b52427124599c9f6c935581991c777a384 Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Tue, 26 May 2026 05:54:15 -0400 Subject: [PATCH 1/4] IT WORKS --- Package.resolved | 11 +++++- Package.swift | 9 +++-- Sources/CXLoad/include/CXLoad.h | 7 ++++ Sources/XLoad/InjectionWatcher.swift | 51 ++++++++++++++++++++++++++++ Sources/XLoad/XLoad.swift | 5 ++- 5 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 Sources/XLoad/InjectionWatcher.swift diff --git a/Package.resolved b/Package.resolved index 42b514b..dcf5136 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "719e2a9c2073e0d449a81e92369494d7d9ac5a83957a66f5e497424604465b76", + "originHash" : "c293f4c78cf631fd248fb31d7664f61c5c3618d0bfbfcf862e6f3712bb8925d3", "pins" : [ { "identity" : "dlkit", @@ -10,6 +10,15 @@ "version" : "3.5.7" } }, + { + "identity" : "ellekit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kabiroberai/ellekit", + "state" : { + "branch" : "swiftpm-fixes", + "revision" : "3468e6b2261f059c784c6d37ac3425671c018a1d" + } + }, { "identity" : "fishhook", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index d7743fe..f82e8fb 100644 --- a/Package.swift +++ b/Package.swift @@ -5,8 +5,8 @@ import PackageDescription let package = Package( name: "xload", platforms: [ - .macOS(.v14), - .iOS(.v17), + .macOS(.v15), + .iOS(.v18), ], products: [ .library( @@ -25,6 +25,10 @@ let package = Package( branch: "debug-trait", traits: [], // disable DEBUG_ONLY ), + .package( + url: "https://github.com/kabiroberai/ellekit", + branch: "swiftpm-fixes", + ), ], targets: [ .target( @@ -35,6 +39,7 @@ let package = Package( dependencies: [ "CXLoad", .product(name: "InjectionImpl", package: "InjectionLite"), + .product(name: "ellekit", package: "ellekit"), ] ), ], diff --git a/Sources/CXLoad/include/CXLoad.h b/Sources/CXLoad/include/CXLoad.h index febe1c8..2cbba2b 100644 --- a/Sources/CXLoad/include/CXLoad.h +++ b/Sources/CXLoad/include/CXLoad.h @@ -1,6 +1,13 @@ #ifndef CXLoad_h #define CXLoad_h +#include + +typedef struct { + const void *value; + uint8_t state; +} AGValue; + void xload_load(); #endif diff --git a/Sources/XLoad/InjectionWatcher.swift b/Sources/XLoad/InjectionWatcher.swift new file mode 100644 index 0000000..756bafa --- /dev/null +++ b/Sources/XLoad/InjectionWatcher.swift @@ -0,0 +1,51 @@ +import Foundation +import CXLoad +import Synchronization +import ellekit + +enum SwiftUIInterceptor { + fileprivate typealias CGetValue = @convention(c) (UInt32, UInt32, UnsafeRawPointer?) -> AGValue + + static func install() { + let target = dlsym(dlopen(nil, 0), "AGGraphGetValue")! + let hooked = unsafeBitCast(hookedGetValue as CGetValue, to: UnsafeMutableRawPointer.self) + + _orig.withLock { + // we have to hook inside the lock otherwise there's + // a brief period between when we install the hook and + // when we set orig. if someone calls AGGraphGetValue during that time, + // we'll crash. + let res = ellekit::hook(target, hooked) + $0 = unsafeBitCast(res, to: CGetValue.self) + } + } + + fileprivate static let _orig = Mutex(nil) + // "upgrades" the mutex to an atomic. safe because we never read it before it's set. + fileprivate static let orig = SwiftUIInterceptor._orig.withLock(\.self)! +} + +@c private func hookedGetValue( + _ attribute: UInt32, + _ options: UInt32, + _ type: UnsafeRawPointer?, +) -> AGValue { + if Thread.isMainThread { + MainActor.assumeIsolated { _ = InjectionWatcher.shared.id } + } + let origFunc = SwiftUIInterceptor.orig + return origFunc(attribute, options, type) +} + +@Observable @MainActor +final class InjectionWatcher { + private(set) var id: UInt64 = 0 + + static let shared = InjectionWatcher() + + private init() {} + + func bump() { + InjectionWatcher.shared.id &+= 1 + } +} diff --git a/Sources/XLoad/XLoad.swift b/Sources/XLoad/XLoad.swift index 242308b..a4477fc 100644 --- a/Sources/XLoad/XLoad.swift +++ b/Sources/XLoad/XLoad.swift @@ -20,6 +20,8 @@ private func load() async throws { throw StringError("Did not receive XLOAD_WATCH_DIR") } + SwiftUIInterceptor.install() + let changes = try await watchDirectory(watchPath) let reloader = FileReloader() @@ -54,12 +56,13 @@ private func watchDirectory(_ path: String) async throws -> AsyncStream { actor FileReloader { private var loader = Reloader() - func reload(_ dylib: String) { + func reload(_ dylib: String) async { guard let (image, classes) = loader.loadAndPatch(in: dylib) else { print("[XLoad] Failed to load image: \(dylib)") return } loader.sweeper.sweepAndRunTests(image: image, classes: classes) + await InjectionWatcher.shared.bump() } } From db52af5d01f329ee86f2766976277bdb40cfdea9 Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Tue, 26 May 2026 06:25:39 -0400 Subject: [PATCH 2/4] simplify --- Sources/XLoad/InjectionWatcher.swift | 18 +++++++++++------- Sources/XLoad/XLoad.swift | 2 +- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/Sources/XLoad/InjectionWatcher.swift b/Sources/XLoad/InjectionWatcher.swift index 756bafa..d80f01c 100644 --- a/Sources/XLoad/InjectionWatcher.swift +++ b/Sources/XLoad/InjectionWatcher.swift @@ -30,22 +30,26 @@ enum SwiftUIInterceptor { _ options: UInt32, _ type: UnsafeRawPointer?, ) -> AGValue { - if Thread.isMainThread { - MainActor.assumeIsolated { _ = InjectionWatcher.shared.id } - } + // AGGraphGetValue is called on basically every view render (afaict; I hope). + // by registering an Observation access in this method, we make it so any time + // we call bump(), it triggers an update on every SwiftUI view in the app. + InjectionWatcher.shared.observe() let origFunc = SwiftUIInterceptor.orig return origFunc(attribute, options, type) } -@Observable @MainActor -final class InjectionWatcher { - private(set) var id: UInt64 = 0 +struct InjectionWatcher: Observable, Sendable { + private let registrar = ObservationRegistrar() static let shared = InjectionWatcher() private init() {} + func observe() { + registrar.access(self, keyPath: \.registrar) + } + func bump() { - InjectionWatcher.shared.id &+= 1 + registrar.withMutation(of: self, keyPath: \.registrar) {} } } diff --git a/Sources/XLoad/XLoad.swift b/Sources/XLoad/XLoad.swift index a4477fc..560abd6 100644 --- a/Sources/XLoad/XLoad.swift +++ b/Sources/XLoad/XLoad.swift @@ -62,7 +62,7 @@ actor FileReloader { return } loader.sweeper.sweepAndRunTests(image: image, classes: classes) - await InjectionWatcher.shared.bump() + InjectionWatcher.shared.bump() } } From 1ca72cf9fbb594914d3e9ce46f7c123033414cba Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Wed, 27 May 2026 00:26:45 -0400 Subject: [PATCH 3/4] changes --- Sources/XLoad/InjectionWatcher.swift | 22 +++++++++++++++------- Sources/XLoad/XLoad.swift | 19 ++++++++++++++----- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/Sources/XLoad/InjectionWatcher.swift b/Sources/XLoad/InjectionWatcher.swift index d80f01c..035d69e 100644 --- a/Sources/XLoad/InjectionWatcher.swift +++ b/Sources/XLoad/InjectionWatcher.swift @@ -33,23 +33,31 @@ enum SwiftUIInterceptor { // AGGraphGetValue is called on basically every view render (afaict; I hope). // by registering an Observation access in this method, we make it so any time // we call bump(), it triggers an update on every SwiftUI view in the app. - InjectionWatcher.shared.observe() + InjectionWatcher.shared.subscribe() let origFunc = SwiftUIInterceptor.orig return origFunc(attribute, options, type) } -struct InjectionWatcher: Observable, Sendable { +private struct InjectionWatcher: Observable, Sendable { private let registrar = ObservationRegistrar() static let shared = InjectionWatcher() - private init() {} + private init() { + Task { [self] in await watch() } + } + + private func watch() async { + for await _ in NotificationCenter.default.notifications(named: .init("INJECTION_BUNDLE_NOTIFICATION")) { + notify() + } + } - func observe() { - registrar.access(self, keyPath: \.registrar) + private func notify() { + registrar.withMutation(of: self, keyPath: \.self) {} } - func bump() { - registrar.withMutation(of: self, keyPath: \.registrar) {} + func subscribe() { + registrar.access(self, keyPath: \.self) } } diff --git a/Sources/XLoad/XLoad.swift b/Sources/XLoad/XLoad.swift index 560abd6..e056c77 100644 --- a/Sources/XLoad/XLoad.swift +++ b/Sources/XLoad/XLoad.swift @@ -16,11 +16,21 @@ import System } private func load() async throws { - guard let watchPath = ProcessInfo.processInfo.environment["XLOAD_WATCH_DIR"] else { - throw StringError("Did not receive XLOAD_WATCH_DIR") - } + loadInterceptor() + try await loadWatcher() +} +private func loadInterceptor() { + guard ProcessInfo.processInfo.environment["XLOAD_INTERCEPT"] == "1" else { return } + print("[XLoad] Loading interceptor...") SwiftUIInterceptor.install() +} + +private func loadWatcher() async throws { + guard let watchPath = ProcessInfo.processInfo.environment["XLOAD_WATCH_DIR"] + else { return } + + print("[XLoad] Loading watcher...") let changes = try await watchDirectory(watchPath) @@ -56,13 +66,12 @@ private func watchDirectory(_ path: String) async throws -> AsyncStream { actor FileReloader { private var loader = Reloader() - func reload(_ dylib: String) async { + func reload(_ dylib: String) { guard let (image, classes) = loader.loadAndPatch(in: dylib) else { print("[XLoad] Failed to load image: \(dylib)") return } loader.sweeper.sweepAndRunTests(image: image, classes: classes) - InjectionWatcher.shared.bump() } } From ece6b9b09e96cd97af053bca4a81f6ae0ecf425b Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Wed, 27 May 2026 00:34:49 -0400 Subject: [PATCH 4/4] tweak --- Sources/XLoad/InjectionWatcher.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/XLoad/InjectionWatcher.swift b/Sources/XLoad/InjectionWatcher.swift index 035d69e..c234c61 100644 --- a/Sources/XLoad/InjectionWatcher.swift +++ b/Sources/XLoad/InjectionWatcher.swift @@ -32,7 +32,7 @@ enum SwiftUIInterceptor { ) -> AGValue { // AGGraphGetValue is called on basically every view render (afaict; I hope). // by registering an Observation access in this method, we make it so any time - // we call bump(), it triggers an update on every SwiftUI view in the app. + // we call notify(), it triggers an update on every SwiftUI view in the app. InjectionWatcher.shared.subscribe() let origFunc = SwiftUIInterceptor.orig return origFunc(attribute, options, type)