Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor SwiftWrapperFactory to InspectableTypeBindingResolver #466

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Docs/Migration from thebrowsercompany's swift-winrt.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ E.g. when `getBase()` returns `IBase` and the application code wants to convert

**v2**: Optional. Pluggable.

**Migration path (medium)**: Provide `swiftWrapperFactory` implementation.
**Migration path (medium)**: Set up a `InspectableTypeBindingResolver`.

### Constructor error handling

Expand Down
2 changes: 1 addition & 1 deletion Docs/Projection Design.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ Swift protocols generated for COM/WinRT interfaces have a "Protocol" suffix. The
**Example**: `class CustomVector: IVectorProtocol { func getView() throws -> IVectorView }`

### Upcasting support
Given a `getObject() -> Base` that actually returns a `Derived`, there is opt-in support for casting `Base` to `Derived` through implementing `SwiftObjectWrapperFactory`.
Given a `getObject() -> Base` that actually returns a `Derived`, there is opt-in support for casting `Base` to `Derived` through setting up an `InspectableTypeBindingResolver`.

**Rationale**: The C# projection supports this and it makes for a more natural use of projections, however it requires costly dynamic wrapper type lookup and instantiation on every method return. A built-in implementation would require lower-level assemblies to know about module names of higher-level assemblies.

Expand Down
2 changes: 1 addition & 1 deletion Generator/Sources/ProjectionModel/SupportModules.swift
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ extension SupportModules.WinRT {
public static var iactivationFactoryBinding: SwiftType { moduleType.member("IActivationFactoryBinding") }

public static var activationFactoryResolverGlobal: String { "\(moduleName).activationFactoryResolver" }
public static var swiftWrapperFactoryGlobal: String { "\(moduleName).swiftWrapperFactory" }
public static var wrapInspectableGlobal: String { "\(moduleName).wrapInspectable" }
}

public enum BuiltInTypeKind {
Expand Down
114 changes: 66 additions & 48 deletions Generator/Sources/SwiftWinRT/Writing/ABIBinding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ internal func writeABIBindingConformance(_ typeDefinition: TypeDefinition, gener
if typeDefinition.genericArity == 0 {
// Non-generic type, create a standard projection type.
// enum IVectorBinding: WinRTBinding... {}
try writeInterfaceOrDelegateBindingType(typeDefinition.bindType(),
try writeInterfaceOrDelegateBindingType(
typeDefinition.bindType(),
name: try projection.toBindingTypeName(typeDefinition),
projection: projection, to: writer)
}
Expand Down Expand Up @@ -370,59 +371,76 @@ fileprivate func writeInterfaceOrDelegateBindingType(
name: String,
projection: Projection,
to writer: some SwiftDeclarationWriter) throws {
precondition(type.definition is InterfaceDefinition || type.definition is DelegateDefinition)
let bindingProtocol = type.definition is InterfaceDefinition
? SupportModules.WinRT.interfaceBinding : SupportModules.WinRT.delegateBinding

// Projections of generic instantiations are not owned by any specific module.
// Making them internal avoids clashes between redundant definitions across modules.
try writer.writeEnum(
attributes: [ Projection.getAvailableAttribute(type.definition) ].compactMap { $0 },
visibility: type.genericArgs.isEmpty ? Projection.toVisibility(type.definition.visibility) : .internal,
name: name,
protocolConformances: [ bindingProtocol ]) { writer throws in

let importClassName = "Import"

if type.definition is InterfaceDefinition {
try writeReferenceTypeBindingConformance(
apiType: type, abiType: type,
wrapImpl: { writer, paramName in
writer.writeStatement("\(importClassName)(_wrapping: consume \(paramName))")
},
projection: projection,
to: writer)
let attributes = try [ Projection.getAvailableAttribute(type.definition) ].compactMap { $0 }
// Generic specializations can exist in multiple modules, so they must be internal.
let visibility = type.genericArgs.isEmpty ? Projection.toVisibility(type.definition.visibility) : SwiftVisibility.internal

if type.definition is InterfaceDefinition {
// Implement binding as a class so it can be looked up using NSClassFromString.
try writer.writeClass(
attributes: attributes, visibility: visibility, name: name,
protocolConformances: [ SupportModules.WinRT.interfaceBinding ]) { writer throws in
try writeInterfaceOrDelegateBindingMembers(
type, bindingType: .named(name),
projection: projection, to: writer)
}
else {
assert(type.definition is DelegateDefinition)
try writeReferenceTypeBindingConformance(
apiType: type, abiType: type,
wrapImpl: { writer, paramName in
writer.writeStatement("\(importClassName)(_wrapping: consume \(paramName)).invoke")
},
toCOMImpl: { writer, paramName in
// Delegates have no identity, so create one for them
writer.writeStatement("ExportedDelegate<Self>(\(paramName)).toCOM()")
},
projection: projection,
to: writer)
}
else {
try writer.writeEnum(
attributes: attributes, visibility: visibility, name: name,
protocolConformances: [ SupportModules.WinRT.delegateBinding ]) { writer throws in
try writeInterfaceOrDelegateBindingMembers(
type, bindingType: .named(name),
projection: projection, to: writer)
}
}
}

try writeCOMImportClass(
type, visibility: .private, name: importClassName,
bindingType: .named(name),
projection: projection, to: writer)
fileprivate func writeInterfaceOrDelegateBindingMembers(
_ type: BoundType,
bindingType: SwiftType,
projection: Projection,
to writer: SwiftTypeDefinitionWriter) throws {
precondition(type.definition is InterfaceDefinition || type.definition is DelegateDefinition)

// public static var exportedVirtualTable: VirtualTablePointer { .init(&virtualTable) }
writer.writeComputedProperty(
visibility: .public, static: true, name: "exportedVirtualTable",
type: SupportModules.COM.virtualTablePointer) { writer in
writer.writeStatement(".init(&virtualTable)")
}
let importClassName = "Import"

if type.definition is InterfaceDefinition {
try writeReferenceTypeBindingConformance(
apiType: type, abiType: type,
wrapImpl: { writer, paramName in
writer.writeStatement("\(importClassName)(_wrapping: consume \(paramName))")
},
projection: projection,
to: writer)
}
else {
assert(type.definition is DelegateDefinition)
try writeReferenceTypeBindingConformance(
apiType: type, abiType: type,
wrapImpl: { writer, paramName in
writer.writeStatement("\(importClassName)(_wrapping: consume \(paramName)).invoke")
},
toCOMImpl: { writer, paramName in
// Delegates have no identity, so create one for them
writer.writeStatement("ExportedDelegate<Self>(\(paramName)).toCOM()")
},
projection: projection,
to: writer)
}

// private static var virtualTable = SWRT_IFoo_VirtualTable(...)
try writeVirtualTableProperty(name: "virtualTable", abiType: type, swiftType: type, projection: projection, to: writer)
try writeCOMImportClass(
type, visibility: .private, name: importClassName, bindingType: bindingType, projection: projection, to: writer)

// public static var exportedVirtualTable: VirtualTablePointer { .init(&virtualTable) }
writer.writeComputedProperty(
visibility: .public, static: true, name: "exportedVirtualTable",
type: SupportModules.COM.virtualTablePointer) { writer in
writer.writeStatement(".init(&virtualTable)")
}

// private static var virtualTable = SWRT_IFoo_VirtualTable(...)
try writeVirtualTableProperty(name: "virtualTable", abiType: type, swiftType: type, projection: projection, to: writer)
}

internal func writeTypeNameProperty(type: BoundType, to writer: SwiftTypeDefinitionWriter) throws {
Expand Down
40 changes: 4 additions & 36 deletions InteropTests/Tests/ClassInheritanceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,42 +76,10 @@ class ClassInheritanceTests : XCTestCase {
}

public func testWithUpcasting() throws {
struct UpcastableSwiftWrapperFactory: SwiftWrapperFactory {
func create<StaticBinding: COMBinding>(
_ reference: consuming StaticBinding.ABIReference,
staticBinding: StaticBinding.Type) -> StaticBinding.SwiftObject {
// Try from the runtime type first, then fall back to the statically known binding
if let object: StaticBinding.SwiftObject = fromRuntimeType(
inspectable: IInspectablePointer(OpaquePointer(reference.pointer))) {
return object
} else {
return StaticBinding._wrap(consume reference)
}
}

func fromRuntimeType<SwiftObject>(inspectable: IInspectablePointer) -> SwiftObject? {
guard let runtimeClassName = try? COMInterop(inspectable).getRuntimeClassName() else { return nil }
let swiftBindingQualifiedName = toBindingQualifiedName(runtimeClassName: consume runtimeClassName)
guard let bindingType = NSClassFromString(swiftBindingQualifiedName) as? any RuntimeClassBinding.Type else { return nil }
return try? bindingType._wrapObject(COMReference(addingRef: inspectable)) as? SwiftObject
}

func toBindingQualifiedName(runtimeClassName: String) -> String {
// Name.Space.ClassName -> WinRTComponent.NameSpace_ClassNameBinding
var result = runtimeClassName.replacingOccurrences(of: ".", with: "_")
if let lastDotIndex = runtimeClassName.lastIndex(of: ".") {
result = result.replacingCharacters(in: lastDotIndex...lastDotIndex, with: "_")
}
result = result.replacingOccurrences(of: ".", with: "")
result.insert(contentsOf: "WinRTComponent.", at: result.startIndex)
result += "Binding"
return result
}
}

let originalFactory = WindowsRuntime.swiftWrapperFactory
WindowsRuntime.swiftWrapperFactory = UpcastableSwiftWrapperFactory()
defer { WindowsRuntime.swiftWrapperFactory = originalFactory }
let originalBindingResolver = WindowsRuntime.inspectableTypeBindingResolver
WindowsRuntime.inspectableTypeBindingResolver = DefaultInspectableTypeBindingResolver(
namespacesToModuleNames: ["WinRTComponent": "WinRTComponent"])
defer { WindowsRuntime.inspectableTypeBindingResolver = originalBindingResolver }

XCTAssertNotNil(try WinRTComponent_MinimalBaseClassHierarchy.createUnsealedDerivedAsBase() as? WinRTComponent_MinimalUnsealedDerivedClass)
XCTAssertNotNil(try WinRTComponent_MinimalBaseClassHierarchy.createSealedDerivedAsBase() as? WinRTComponent_MinimalSealedDerivedClass)
Expand Down
6 changes: 3 additions & 3 deletions Support/Sources/COM/COMBinding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ public protocol COMBinding: ABIBinding where SwiftValue == SwiftObject?, ABIValu
/// Gets the COM interface identifier.
static var interfaceID: COMInterfaceID { get }

// Non-nullable overload
/// Converts a Swift object to its COM ABI representation.
static func toCOM(_ object: SwiftObject) throws -> ABIReference

// Attempts un unwrap a COM pointer into an existing Swift object.
/// Attempts un unwrap a COM pointer into an existing Swift object.
static func _unwrap(_ pointer: ABIPointer) -> SwiftObject?

// Wraps a COM object into a new Swift object, without attempting to unwrap it first.
/// Wraps a COM object into a new Swift object, without attempting to unwrap it first.
static func _wrap(_ reference: consuming ABIReference) -> SwiftObject
}

Expand Down
19 changes: 14 additions & 5 deletions Support/Sources/WindowsRuntime/BindingProtocols+extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,20 @@ extension ReferenceTypeBinding {
}
}

extension InspectableTypeBinding {
public static func _wrapInspectable(_ reference: consuming IInspectableReference) throws -> IInspectable {
try _wrap(reference.queryInterface(interfaceID)) as! IInspectable
}
}

extension InterfaceBinding {
public static func fromABI(consuming value: inout ABIValue) -> SwiftValue {
guard let pointer = value else { return nil }
let reference = COMReference(transferringRef: pointer)
if let swiftObject = _unwrap(reference.pointer) { return swiftObject }
return wrapInspectable(reference, staticBinding: Self.self)
}

// Shadow COMTwoWayBinding methods to use WinRTError instead of COMError
public static func _implement<This>(_ this: UnsafeMutablePointer<This>?, _ body: (SwiftObject) throws -> Void) -> SWRT_HResult {
guard let this else { return WinRTError.toABI(hresult: HResult.pointer, message: "WinRT 'this' pointer was null") }
Expand Down Expand Up @@ -68,10 +81,6 @@ extension DelegateBinding {
}

extension RuntimeClassBinding {
public static func _wrapObject(_ reference: consuming IInspectableReference) throws -> IInspectable {
try _wrap(reference.queryInterface(interfaceID)) as! IInspectable
}

// Shadow COMTwoWayBinding methods to use WinRTError instead of COMError
public static func _implement<This>(_ this: UnsafeMutablePointer<This>?, _ body: (SwiftObject) throws -> Void) -> SWRT_HResult {
guard let this else { return WinRTError.toABI(hresult: HResult.pointer, message: "WinRT 'this' pointer was null") }
Expand All @@ -92,7 +101,7 @@ extension ComposableClassBinding {
guard let pointer = value else { return nil }
let reference = COMReference(transferringRef: pointer)
if let swiftObject = _unwrap(reference.pointer) { return swiftObject }
return swiftWrapperFactory.create(reference, staticBinding: Self.self)
return wrapInspectable(reference, staticBinding: Self.self)
}

public static func _unwrap(_ pointer: ABIPointer) -> SwiftObject? {
Expand Down
16 changes: 9 additions & 7 deletions Support/Sources/WindowsRuntime/BindingProtocols.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,20 @@ public protocol StructBinding: ValueTypeBinding {} // POD structs will also conf
/// Protocol for bindings of WinRT reference types into Swift.
public protocol ReferenceTypeBinding: WinRTBinding, COMBinding {}

/// Protocol for bindings of WinRT interfaces into Swift.
public protocol InterfaceBinding: ReferenceTypeBinding, COMTwoWayBinding {} // where SwiftObject: any IInspectable

/// Protocol for bindings of WinRT delegates into Swift.
public protocol DelegateBinding: ReferenceTypeBinding, IReferenceableBinding, COMTwoWayBinding {}

/// Protocol for bindings of non-static WinRT runtime classes into Swift.
/// Protocol for bindings of WinRT types implementing IInspectable into Swift (interfaces and runtime classes).
/// Allows for dynamic instantiation of wrappers for WinRT objects.
/// Conforms to AnyObject so that conforming types must be classes, which can be looked up using NSClassFromString.
public protocol RuntimeClassBinding: ReferenceTypeBinding, AnyObject { // where SwiftObject: IInspectable
static func _wrapObject(_ reference: consuming IInspectableReference) throws -> IInspectable
public protocol InspectableTypeBinding: ReferenceTypeBinding { // where SwiftObject: IInspectable
static func _wrapInspectable(_ reference: consuming IInspectableReference) throws -> IInspectable
}

/// Protocol for bindings of WinRT interfaces into Swift.
public protocol InterfaceBinding: InspectableTypeBinding, COMTwoWayBinding {} // where SwiftObject: any IInspectable

/// Protocol for bindings of non-static WinRT runtime classes into Swift.
public protocol RuntimeClassBinding: InspectableTypeBinding {}

/// Protocol for bindings of WinRT composable classes into Swift.
public protocol ComposableClassBinding: RuntimeClassBinding {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import func Foundation.NSClassFromString

/// Creates Swift wrappers for COM objects based on runtime type information,
/// which allows for instantiating subclass wrappers and upcasting.
public class DefaultInspectableTypeBindingResolver: InspectableTypeBindingResolver {
private let namespacesToModuleNames: [Substring: String] // Store keys as substrings for lookup by substring
private var bindingTypeCache: [String: (any InspectableTypeBinding.Type)?] = [:]
private let cacheFailedLookups: Bool

public init(namespacesToModuleNames: [String: String], cacheFailedLookups: Bool = false) {
// Convert keys to substrings for lookup by substring (won't leak a larger string)
var namespaceSubstringsToModuleNames = [Substring: String](minimumCapacity: namespacesToModuleNames.count)
for (namespace, module) in namespacesToModuleNames {
namespaceSubstringsToModuleNames[namespace[...]] = module
}

self.namespacesToModuleNames = namespaceSubstringsToModuleNames
self.cacheFailedLookups = cacheFailedLookups
}

public func resolve(typeName: String) -> (any InspectableTypeBinding.Type)? {
if let cachedBindingType = bindingTypeCache[typeName] { return cachedBindingType }

let bindingType = lookup(typeName: typeName)
if bindingType != nil || cacheFailedLookups {
bindingTypeCache[typeName] = bindingType
}

return bindingType
}

private func lookup(typeName: String) -> (any InspectableTypeBinding.Type)? {
guard let lastDotIndex = typeName.lastIndex(of: ".") else { return nil }
guard let moduleName = toModuleName(namespace: typeName[..<lastDotIndex]) else { return nil }

// ModuleName.NamespaceSubnamespace_TypeNameBinding
var bindingClassName = moduleName
bindingClassName += "."
for typeNameChar in typeName[..<lastDotIndex] {
guard typeNameChar != "." else { continue }
bindingClassName.append(typeNameChar)
}
bindingClassName += "_"
bindingClassName += typeName[typeName.index(after: lastDotIndex)...]
bindingClassName += "Binding"

return NSClassFromString(bindingClassName) as? any InspectableTypeBinding.Type
}

private func toModuleName(namespace: Substring) -> String? {
var namespace = namespace
while true {
guard !namespace.isEmpty else { return nil }
if let module = namespacesToModuleNames[namespace] { return module }
guard let lastDotIndex = namespace.lastIndex(of: ".") else { return nil }
namespace = namespace[..<lastDotIndex]
}
}
}
17 changes: 0 additions & 17 deletions Support/Sources/WindowsRuntime/SwiftWrapperFactory.swift

This file was deleted.

Loading
Loading