Skip to content

Commit

Permalink
Only enable hot reloading when SCUI_HOT_RELOADING or SWIFT_BUNDLER_HO…
Browse files Browse the repository at this point in the history
…T_RELOADING is present
  • Loading branch information
stackotter committed May 13, 2024
1 parent 1b9565c commit 698161f
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 94 deletions.
17 changes: 14 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,17 @@ if let backend = ProcessInfo.processInfo.environment["SCUI_DEFAULT_BACKEND"] {
#endif
}

let hotReloadingEnabled =
ProcessInfo.processInfo.environment["SWIFT_BUNDLER_HOT_RELOADING"] != nil
|| ProcessInfo.processInfo.environment["SCUI_HOT_RELOADING"] != nil

var swiftSettings: [SwiftSetting] = []
if hotReloadingEnabled {
swiftSettings += [
.define("HOT_RELOADING_ENABLED")
]
}

var libraryType: Product.Library.LibraryType?
switch ProcessInfo.processInfo.environment["SCUI_LIBRARY_TYPE"] {
case "static":
Expand All @@ -71,8 +82,7 @@ switch ProcessInfo.processInfo.environment["SCUI_LIBRARY_TYPE"] {
print("Invalid SCUI_LIBRARY_TYPE, expected static, dynamic, or auto")
libraryType = nil
case nil:
let hotReloading = ProcessInfo.processInfo.environment["SWIFT_BUNDLER_HOT_RELOADING"] != nil
if hotReloading {
if hotReloadingEnabled {
libraryType = .dynamic
} else {
libraryType = nil
Expand Down Expand Up @@ -193,7 +203,8 @@ let package = Package(
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
.product(name: "MacroToolkit", package: "swift-macro-toolkit"),
]
],
swiftSettings: swiftSettings
),
] + swift510Targets
)
Expand Down
145 changes: 77 additions & 68 deletions Sources/HotReloadingMacrosPlugin/HotReloadableAppMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,28 @@ extension HotReloadableAppMacro: PeerMacro {
throw MacroError("@HotReloadable can only be applied to structs")
}

return [
"""
var hotReloadingImportedEntryPoint: (@convention(c) (UnsafeRawPointer, Int) -> Any)? = nil
""",
"""
@_cdecl("body")
public func hotReloadingExportedEntryPoint(app: UnsafeRawPointer, viewId: Int) -> Any {
hotReloadingHasConnectedToServer = true
let app = app.assumingMemoryBound(to: \(raw: structDecl.identifier).self)
return SwiftCrossUI.HotReloadableView(
app.pointee.entryPoint(viewId: viewId)
)
}
""",
"""
var hotReloadingHasConnectedToServer = false
""",
]
#if HOT_RELOADING_ENABLED
return [
"""
var hotReloadingImportedEntryPoint: (@convention(c) (UnsafeRawPointer, Int) -> Any)? = nil
""",
"""
@_cdecl("body")
public func hotReloadingExportedEntryPoint(app: UnsafeRawPointer, viewId: Int) -> Any {
hotReloadingHasConnectedToServer = true
let app = app.assumingMemoryBound(to: \(raw: structDecl.identifier).self)
return SwiftCrossUI.HotReloadableView(
app.pointee.entryPoint(viewId: viewId)
)
}
""",
"""
var hotReloadingHasConnectedToServer = false
""",
]
#else
return []
#endif
}
}

Expand Down Expand Up @@ -59,67 +63,72 @@ extension HotReloadableAppMacro: MemberMacro {
throw MacroError("@HotReloadable can only be applied to structs")
}

// TODO: Skip nested declarations
let visitor = HotReloadableViewVisitor(viewMode: .fixedUp)
visitor.walk(structDecl._syntax)
#if HOT_RELOADING_ENABLED
// TODO: Skip nested declarations
let visitor = HotReloadableViewVisitor(viewMode: .fixedUp)
visitor.walk(structDecl._syntax)

let cases: [DeclSyntax] = visitor.hotReloadableExprs.enumerated().map { (index, expr) in
"""
if viewId == \(raw: index.description) {
return SwiftCrossUI.HotReloadableView(\(expr))
let cases: [DeclSyntax] = visitor.hotReloadableExprs.enumerated().map { (index, expr) in
"""
if viewId == \(raw: index.description) {
return SwiftCrossUI.HotReloadableView(\(expr))
}
"""
}
"""
}
var exprIds: [String] = try visitor.hotReloadableExprs.enumerated().map { (index, expr) in
guard let location = context.location(of: expr) else {
throw MacroError(
"hotReloadable expr without source location?? (shouldn't be possible)"
)
var exprIds: [String] = try visitor.hotReloadableExprs.enumerated().map {
(index, expr) in
guard let location = context.location(of: expr) else {
throw MacroError(
"hotReloadable expr without source location?? (shouldn't be possible)"
)
}
return "ExprLocation(line: \(location.line), column: \(location.column)): \(index),"
}
return "ExprLocation(line: \(location.line), column: \(location.column)): \(index),"
}

// Handle empty dictionary literal
if exprIds.isEmpty {
exprIds.append(":")
}
// Handle empty dictionary literal
if exprIds.isEmpty {
exprIds.append(":")
}

return [
"""
func entryPoint(viewId: Int) -> SwiftCrossUI.HotReloadableView {
#if !canImport(SwiftBundlerRuntime)
#error("Hot reloading requires importing SwiftBundlerRuntime from the swift-bundler package")
#endif
return [
"""
func entryPoint(viewId: Int) -> SwiftCrossUI.HotReloadableView {
#if !canImport(SwiftBundlerRuntime)
#error("Hot reloading requires importing SwiftBundlerRuntime from the swift-bundler package")
#endif
if !hotReloadingHasConnectedToServer {
hotReloadingHasConnectedToServer = true
Task {
do {
var client = try await HotReloadingClient()
print("Hot reloading: received new dylib")
try await client.handlePackets { dylib in
guard let symbol = dylib.symbol(named: "body", ofType: (@convention(c) (UnsafeRawPointer, Int) -> Any).self) else {
print("Hot reloading: Missing 'body' symbol")
return
if !hotReloadingHasConnectedToServer {
hotReloadingHasConnectedToServer = true
Task {
do {
var client = try await HotReloadingClient()
print("Hot reloading: received new dylib")
try await client.handlePackets { dylib in
guard let symbol = dylib.symbol(named: "body", ofType: (@convention(c) (UnsafeRawPointer, Int) -> Any).self) else {
print("Hot reloading: Missing 'body' symbol")
return
}
hotReloadingImportedEntryPoint = symbol
_forceRefresh()
}
hotReloadingImportedEntryPoint = symbol
_forceRefresh()
} catch {
print("Hot reloading: \\(error)")
}
} catch {
print("Hot reloading: \\(error)")
}
}
\(raw: cases.map(\.description).joined(separator: "\n"))
fatalError("Unknown viewId \\(viewId)")
}
\(raw: cases.map(\.description).joined(separator: "\n"))
fatalError("Unknown viewId \\(viewId)")
}
""",
"""
static let hotReloadingExprIds: [ExprLocation: Int] = [
\(raw: exprIds.joined(separator: "\n"))
""",
"""
static let hotReloadingExprIds: [ExprLocation: Int] = [
\(raw: exprIds.joined(separator: "\n"))
]
""",
]
""",
]
#else
return []
#endif
}
}
49 changes: 27 additions & 22 deletions Sources/HotReloadingMacrosPlugin/HotReloadableExprMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,33 @@ public struct HotReloadableExprMacro: ExpressionMacro {
guard let expr = destructureSingle(node.arguments), expr.label == nil else {
throw MacroError("#hotReloadable takes exactly one unlabelled argument")
}
guard let location = context.location(of: expr) else {
throw MacroError(
"#hotReloadable expr without source location?? (shouldn't be possible)")
}
// TODO: Guard against use of `#hotReloadable` in situations where `self` doesn't refer
// to the root App type of the user's application.
return
"""
{
let location = ExprLocation(line: \(location.line), column: \(location.column))
let viewId = Self.hotReloadingExprIds[location]!
if let hotReloadingImportedEntryPoint {
return withUnsafePointer(to: self) { pointer in
return hotReloadingImportedEntryPoint(
pointer,
viewId
) as! SwiftCrossUI.HotReloadableView

#if HOT_RELOADING_ENABLED
guard let location = context.location(of: expr) else {
throw MacroError(
"#hotReloadable expr without source location?? (shouldn't be possible)")
}
// TODO: Guard against use of `#hotReloadable` in situations where `self` doesn't refer
// to the root App type of the user's application.
return
"""
{
let location = ExprLocation(line: \(location.line), column: \(location.column))
let viewId = Self.hotReloadingExprIds[location]!
if let hotReloadingImportedEntryPoint {
return withUnsafePointer(to: self) { pointer in
return hotReloadingImportedEntryPoint(
pointer,
viewId
) as! SwiftCrossUI.HotReloadableView
}
} else {
return self.entryPoint(viewId: viewId)
}
} else {
return self.entryPoint(viewId: viewId)
}
}()
"""
}()
"""
#else
return "SwiftCrossUI.HotReloadableView(\(expr))"
#endif
}
}
2 changes: 1 addition & 1 deletion Sources/SwiftCrossUI/HotReloadingMacros.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public macro HotReloadable() =
#externalMacro(module: "HotReloadingMacrosPlugin", type: "HotReloadableAppMacro")

@freestanding(expression)
public macro hotReloadable<T>(_ expr: T) -> HotReloadableView =
public macro hotReloadable<T: View>(_ expr: T) -> HotReloadableView =
#externalMacro(module: "HotReloadingMacrosPlugin", type: "HotReloadableExprMacro")

public struct ExprLocation: Hashable {
Expand Down

0 comments on commit 698161f

Please sign in to comment.