1
0
Fork 0

Bind C array in-parameters, finish cross-module shadow migration

This commit is contained in:
Brendan Szymanski 2026-08-01 23:40:58 -04:00
parent 745a4c3401
commit a172d07ab1
47 changed files with 1301 additions and 2584 deletions

View file

@ -40,8 +40,16 @@ public enum MarshalIn: Equatable, Sendable {
case boolToGboolean
/// Convert `String` to a C string (`withCString` / `utf8` pointer).
case stringToC
/// Convert `[String]` to a NULL-terminated C `char **`.
/// Convert `[String]` to a NULL-terminated C `char **` (mutable outer + elements).
case stringArrayToC
/// Convert `[String]` to a NULL-terminated C `const char * const *` (const outer + elements).
case stringConstArrayToC
/// Convert `[String]` to a NULL-terminated C `const char **` (const elements, mutable outer pointer).
case stringConstElementArrayToC
/// Convert `[Wrapper]` to a C array of instance pointers (`T **`), NULL-terminated.
case objectArrayToC
/// Convert `[T]` of C-scalar values to a contiguous C buffer (`const T *`).
case scalarArrayToC
/// Access the underlying pointer of an object or interface wrapper.
case objectPointer
/// Access the underlying pointer of an interface-typed wrapper (a
@ -833,4 +841,34 @@ extension Mapping {
static let stringArrayMapping = Mapping(
swiftType: "[String]", cSwiftType: "UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>?",
marshalIn: .stringArrayToC, marshalOut: .unsupported(reason: "C array return not yet bridged"))
/// Maps a `[String]` parameter to a NULL-terminated C `const char * const *` array.
static let constStringArrayMapping = Mapping(
swiftType: "[String]", cSwiftType: "UnsafePointer<UnsafePointer<CChar>?>?",
marshalIn: .stringConstArrayToC, marshalOut: .unsupported(reason: "C array return not yet bridged"))
/// Maps a `[String]` parameter to a C `const char **` (const elements,
/// mutable outer pointer), NULL-terminated.
static let constElementStringArrayMapping = Mapping(
swiftType: "[String]", cSwiftType: "UnsafeMutablePointer<UnsafePointer<CChar>?>?",
marshalIn: .stringConstElementArrayToC,
marshalOut: .unsupported(reason: "C array return not yet bridged"))
/// Maps a C array of object/interface pointers to `[Wrapper]`.
/// - Parameter element: The already-mapped element type.
static func objectArrayMapping(element: Mapping) -> Mapping {
Mapping(swiftType: "[\(element.swiftType)]", cSwiftType: "UnsafeMutableRawPointer?",
marshalIn: .objectArrayToC,
marshalOut: .unsupported(reason: "C array return not yet bridged"),
category: element.category)
}
/// Maps a contiguous C buffer of scalars to `[T]`.
/// - Parameter element: The already-mapped element type.
static func scalarArrayMapping(element: Mapping) -> Mapping {
Mapping(swiftType: "[\(element.swiftType)]",
cSwiftType: "UnsafeMutablePointer<\(element.swiftType)>?",
marshalIn: .scalarArrayToC,
marshalOut: .unsupported(reason: "C array return not yet bridged"))
}
}

View file

@ -888,6 +888,10 @@ public struct ArrayInfo: Equatable, Sendable {
public var isZeroTerminated: Bool
/// The C type spelling of the array itself (e.g. `"char**"`), when present.
public var cType: String
/// The C type spelling of the array's *element* (e.g. `"AdwNavigationPage*"`,
/// `"guint8"`), when the GIR `<array>`'s child `<type>` carries one. Empty
/// when absent; callers then infer element depth from `cType` minus one.
public var elementCType: String
/// Creates array length metadata.
///
@ -897,12 +901,15 @@ public struct ArrayInfo: Equatable, Sendable {
/// - isZeroTerminated: Whether a zero/NULL terminator delimits the array.
/// Defaults to `false`.
/// - cType: The C type spelling of the array. Defaults to `""`.
/// - elementCType: The C type spelling of the array's element. Defaults to `""`.
public init(lengthParameterIndex: Int? = nil, fixedSize: Int? = nil,
isZeroTerminated: Bool = false, cType: String = "") {
isZeroTerminated: Bool = false, cType: String = "",
elementCType: String = "") {
self.lengthParameterIndex = lengthParameterIndex
self.fixedSize = fixedSize
self.isZeroTerminated = isZeroTerminated
self.cType = cType
self.elementCType = elementCType
}
/// Whether the array's length can be determined at all.

View file

@ -378,6 +378,58 @@ private func renderSupport(moduleName: String, dependencyModules: [String] = [],
return try cStrings.withUnsafeMutableBufferPointer { try body($0.baseAddress) }
}
/// Bridges a `[String]` to a NULL-terminated C `const char * const *`, freeing the copies after `body`.
@inline(__always)
func _withConstStringArray<R>(_ strings: [String], _ body: (UnsafePointer<UnsafePointer<CChar>?>?) throws -> R) rethrows -> R {
var cStrings: [UnsafeMutablePointer<CChar>?] = strings.map { g_strdup($0) }
cStrings.append(nil)
defer { for p in cStrings { g_free(p) } }
return try cStrings.withUnsafeMutableBufferPointer { buffer in
try buffer.withMemoryRebound(to: (UnsafePointer<CChar>?).self) { rebound in
try body(rebound.baseAddress)
}
}
}
/// Bridges a `[String]` to a C `const char **` (const elements, mutable outer
/// pointer), NULL-terminated, freeing the copies after `body`.
@inline(__always)
func _withConstElementStringArray<R>(_ strings: [String], _ body: (UnsafeMutablePointer<UnsafePointer<CChar>?>?) throws -> R) rethrows -> R {
var cStrings: [UnsafeMutablePointer<CChar>?] = strings.map { g_strdup($0) }
cStrings.append(nil)
defer { for p in cStrings { g_free(p) } }
return try cStrings.withUnsafeMutableBufferPointer { buffer in
try buffer.withMemoryRebound(to: (UnsafePointer<CChar>?).self) { rebound in
try body(rebound.baseAddress)
}
}
}
/// Bridges wrapper instance pointers to a NULL-terminated C array of element
/// pointers (`T **`), valid only for the duration of `body`. The element
/// pointer type `E` is inferred from the C function being called, which is
/// how both opaque (`OpaquePointer?`) and complete-struct
/// (`UnsafeMutablePointer<GtkWidget>?`) element types are served by one helper.
@inline(__always)
func _withPointerArray<E, R>(_ pointers: [UnsafeMutableRawPointer], _ body: (UnsafeMutablePointer<E>?) throws -> R) rethrows -> R {
var raw: [UnsafeMutableRawPointer?] = pointers
raw.append(nil)
return try raw.withUnsafeMutableBufferPointer { buffer in
try buffer.withMemoryRebound(to: E.self) { rebound in
try body(rebound.baseAddress)
}
}
}
/// Bridges a `[T]` of C-scalar values to a contiguous C buffer valid only for
/// the duration of `body`. The buffer is a copy: only `const` C parameters
/// may be bridged this way (the planner enforces that).
@inline(__always)
func _withScalarArray<T, R>(_ values: [T], _ body: (UnsafeMutablePointer<T>?) throws -> R) rethrows -> R {
var copy = values
return try copy.withUnsafeMutableBufferPointer { try body($0.baseAddress) }
}
\(glibError)\(gtypeConstants)\(closureBoxSupport)\(signalSupport)\(primitiveShims)
"""
}
@ -982,17 +1034,35 @@ private func swiftSignature(_ plan: CallablePlan) -> String {
/// How a string/array parameter is wrapped for the C call.
private enum CWrapKind { case plain, optional, array }
private enum CWrapKind { case plain, optional, array, constArray, constElementStringArray, pointerArray, scalarArray }
/// Generates the closure opener for a string/array parameter: `withCString`,
/// `_withOptionalCString`, or `_withStringArray`.
/// `_withOptionalCString`, `_withStringArray`, `_withConstStringArray`,
/// `_withConstElementStringArray`, `_withPointerArray`, or `_withScalarArray`.
private func cWrapOpener(_ cName: String, _ swiftName: String, _ kind: CWrapKind) -> String {
switch kind {
case .plain: return "\(swiftName).withCString { \(cName) in"
case .optional: return "_withOptionalCString(\(swiftName)) { \(cName) in"
case .array: return "_withStringArray(\(swiftName)) { \(cName) in"
case .constArray: return "_withConstStringArray(\(swiftName)) { \(cName) in"
case .constElementStringArray: return "_withConstElementStringArray(\(swiftName)) { \(cName) in"
case .pointerArray: return "_withPointerArray(\(swiftName).map { $0.pointer }) { \(cName) in"
case .scalarArray: return "_withScalarArray(\(swiftName)) { \(cName) in"
}
}
/// The C-buffer wrapping kind for an array `MarshalIn`, or `nil` when the
/// parameter is not an array.
private func arrayWrapKind(_ marshalIn: MarshalIn) -> CWrapKind? {
switch marshalIn {
case .stringArrayToC: return .array
case .stringConstArrayToC: return .constArray
case .stringConstElementArrayToC: return .constElementStringArray
case .objectArrayToC: return .pointerArray
case .scalarArrayToC: return .scalarArray
default: return nil
}
}
/// Builds the C call: the C-function-call string with each argument marshalled,
/// plus the string parameters that must be wrapped in `withCString`. The
/// instance parameter if any is passed as `self.pointer`.
@ -1026,9 +1096,9 @@ private func cArguments(_ plan: CallablePlan, error: Bool = false) -> (cCall: St
let kind: CWrapKind = param.mapping.swiftType.hasSuffix("?") ? .optional : .plain
stringParams.append((cName: cName, swiftName: param.swiftName, kind: kind))
cArgExprs.append(cName)
} else if param.mapping.marshalIn == .stringArrayToC {
} else if let kind = arrayWrapKind(param.mapping.marshalIn) {
let cName = "cArray\(stringParams.count)"
stringParams.append((cName: cName, swiftName: param.swiftName, kind: .array))
stringParams.append((cName: cName, swiftName: param.swiftName, kind: kind))
cArgExprs.append(cName)
} else if let dataPtrName = closureDataMap[param.cArgIndex], !isCallbackBox(param) {
cArgExprs.append(dataPtrName)
@ -1560,7 +1630,7 @@ private func marshalCallArg(_ param: ParameterPlan) -> String {
return "\(param.swiftName) ? 1 : 0"
case .stringToC:
return param.swiftName
case .stringArrayToC:
case .stringArrayToC, .stringConstArrayToC, .stringConstElementArrayToC, .objectArrayToC, .scalarArrayToC:
return param.swiftName
case .objectPointer, .interfacePointer:
return pointerArg(param)

View file

@ -605,6 +605,7 @@ private let knownMissingCFunctions: Set<String> = [
// exported by the .so but invisible to the Clang importer.
"g_settings_backend_changed", "g_settings_backend_changed_tree",
"g_settings_backend_get_default", "g_settings_backend_path_changed",
"g_settings_backend_keys_changed",
"g_settings_backend_path_writable_changed", "g_settings_backend_writable_changed",
"g_null_settings_backend_new", "g_memory_settings_backend_new", "g_keyfile_settings_backend_new",
// Declared in <gio/gnetworking.h>, likewise excluded from <gio/gio.h>.
@ -1285,6 +1286,8 @@ func planFunction(_ fn: GlobalFunction, context: MapContext) -> CallablePlanResu
/// `TypeRegistry.overridesAncestorMethod`), requiring Swift's `override`
/// keyword. `nil` for interface methods, which have no class ancestry.
func planMethod(_ method: Method, declaringGIRName: String? = nil, context: MapContext) -> CallablePlanResult {
// Same-module override detection: `overridesAncestorMethod` checks only
// ancestors in the same Swift module, where `override` works on `public`.
let isOverride = declaringGIRName.map { girName in
context.registry.overridesAncestorMethod(
named: method.name,
@ -1292,6 +1295,31 @@ func planMethod(_ method: Method, declaringGIRName: String? = nil, context: MapC
in: girName
)
} ?? false
// Cross-module conflict detection: a method that shadows a non-`open`
// ancestor method from a *different* module must be skipped Swift
// rejects cross-module overrides of `public` methods.
//
// Same-module collisions are legitimate overrides and are NOT handled
// here: they keep their `override` keyword, and the ancestor-dedup
// post-pass in `planModules` drops the ones that would still clash.
// The two checks are disjoint by module, and this one runs first, so a
// method never reaches both.
if !isOverride, let girName = declaringGIRName {
if context.registry.shadowsCrossModuleAncestorMethod(
named: method.name,
paramNames: method.parameters.filter { !$0.isInstanceParameter }.map(\.name),
in: girName
) {
// Symbol and reason match the post-pass's ancestor-dedup skips so
// that both mechanisms report one rule identically: the qualified
// `Namespace.Class.method` form, keyed `.inheritedMember`.
let shadowingName = swiftFunctionName(method.name)
return .skip(SkipEntry(symbol: "\(girName).\(shadowingName)",
cIdentifier: method.cIdentifier,
reason: .inheritedMember,
detail: "method '\(shadowingName)' shadows an identically-selectored ancestor method in a different module"))
}
}
return planCallable(
fullName: "\(context.currentNamespace).\(method.name)",
swiftName: swiftFunctionName(method.name), cIdentifier: method.cIdentifier,
@ -1474,16 +1502,23 @@ func planParameters(
// These are plain gpointer params that would otherwise fail the pointer
// check the callback-box mechanism handles them.
let closureTargets = Set(parameters.compactMap(\.closureIndex))
// Pre-scan length parameters for C arrays of strings (e.g. argc for argv).
// Map the length param's position to the array's Swift name.
// Pre-scan length parameters for in-parameter C arrays. The length C arg is
// synthesized from the Swift array's `count` and dropped from the signature.
let hasInstance = parameters.contains(where: \.isInstanceParameter)
var lengthElision: [Int: String] = [:]
for p in parameters {
if case .cArray(let el, let info) = p.type,
el == .string || el == .filename,
let li = info.lengthParameterIndex {
lengthElision[li + (hasInstance ? 1 : 0)] = swiftParameterName(p.name)
for p in parameters where p.direction == .in {
guard case .cArray(_, let info) = p.type, let li = info.lengthParameterIndex else { continue }
let cIndex = li + (hasInstance ? 1 : 0)
guard cIndex < parameters.count, parameters[cIndex].direction == .in else { continue }
// Two arrays sharing one length arg can only be bridged if the caller
// passes equal counts; nothing enforces that, and a mismatch overruns the
// shorter buffer. Skip rather than guess (e.g. g_spawn_async_with_pipes_and_fds).
if let existing = lengthElision[cIndex] {
return .skip(SkipEntry(symbol: "", cIdentifier: nil,
reason: .arrayBridgingUnimplemented,
detail: "parameters '\(existing)' and '\(p.name)' share length parameter index \(li)"))
}
lengthElision[cIndex] = swiftParameterName(p.name)
}
for (index, param) in parameters.enumerated() {
// The instance parameter is always `self` passed as `self.pointer`,
@ -1569,16 +1604,29 @@ func planParameters(
synthesizedLengthOf: arrName))
continue
}
// Intercept C arrays of strings with a known length param [String].
if case .cArray(let el, let info) = param.type,
el == .string || el == .filename, info.lengthParameterIndex != nil {
plans.append(ParameterPlan(
swiftName: swiftParameterName(param.name), cArgIndex: index,
mapping: .stringArrayMapping))
continue
// Intercept in-parameter C arrays -> Swift [T]. Array returns, out-params,
// and array-typed properties still go through `map`, which rejects them.
if case .cArray(let element, let info) = param.type {
switch Result(catching: { try mapArrayParameter(element: element, info: info,
transfer: param.transferOwnership,
context: context) }) {
case .success(let arrayMapping):
guard arrayMapping.isReadyForCallables else {
return .skip(SkipEntry(symbol: "", cIdentifier: nil, reason: .unknownType,
detail: "array parameter '\(param.name)' element type '\(arrayMapping.swiftType)' not yet generated"))
}
plans.append(ParameterPlan(
swiftName: swiftParameterName(param.name), cArgIndex: index,
mapping: arrayMapping))
continue
case .failure(let error as MapError):
return .skip(SkipEntry(symbol: "", cIdentifier: nil, reason: error.reason,
detail: "parameter '\(param.name)': \(error.detail)"))
case .failure:
return .skip(SkipEntry(symbol: "", cIdentifier: nil, reason: .unknownType,
detail: "parameter '\(param.name)' array type not mappable"))
}
}
// Map the type
let mappingResult = Result { try map(param.type, nullable: param.isNullable,
transfer: param.transferOwnership, context: context) }

View file

@ -145,6 +145,101 @@ public func map(
return result.optionalised(nullable: nullable)
}
/// Maps a `direction="in"` C array parameter to its Swift `[T]` mapping.
///
/// Separate from ``map(_:nullable:transfer:context:cType:)`` because only
/// *input parameters* can be bridged: the renderer builds a temporary C buffer
/// that lives for the duration of the call. Array returns, out-params, and
/// array-typed properties still go through `map`, which rejects every
/// `.cArray`.
///
/// - Parameters:
/// - element: The array's element type from the GIR `<array>`'s child `<type>`.
/// - info: The array's length/termination metadata and C type spellings.
/// - transfer: Ownership transfer declared on the parameter.
/// - context: The resolution context.
/// - Returns: A complete `Mapping` whose `marshalIn` is one of the array cases.
/// - Throws: `MapError` with `.arrayWithoutLength` or `.arrayBridgingUnimplemented`.
public func mapArrayParameter(
element: GIRType, info: ArrayInfo,
transfer: TransferOwnership, context: MapContext
) throws(MapError) -> Mapping {
guard info.hasKnownLength else {
throw MapError(reason: .arrayWithoutLength, detail: "C array has no length annotation")
}
let arrayCType = info.cType.replacingOccurrences(of: " ", with: "")
guard !arrayCType.isEmpty else {
throw MapError(reason: .arrayBridgingUnimplemented, detail: "array has no c:type")
}
let elementDepth = info.elementCType.isEmpty
? cPointerDepth(arrayCType) - 1
: cPointerDepth(info.elementCType)
switch element {
case .string, .filename:
guard elementDepth == 1 else {
throw MapError(reason: .arrayBridgingUnimplemented,
detail: "string array element depth \(elementDepth) is not a string vector")
}
guard transfer != .full else {
throw MapError(reason: .arrayBridgingUnimplemented,
detail: "string array with transfer-ownership=full")
}
if arrayCType.hasSuffix("const*") {
return .constStringArrayMapping
} else if arrayCType.hasPrefix("const") {
return .constElementStringArrayMapping
} else {
return .stringArrayMapping
}
case .int8, .int16, .int32, .uint8, .uint16, .uint32,
.char, .uchar, .unichar, .float, .double:
guard info.lengthParameterIndex != nil else {
throw MapError(reason: .arrayBridgingUnimplemented,
detail: "scalar array has no length parameter")
}
guard arrayCType.contains("const") else {
throw MapError(reason: .arrayBridgingUnimplemented,
detail: "scalar array C type '\(info.cType)' is not const (may be an output buffer)")
}
guard elementDepth == 0 else {
throw MapError(reason: .arrayBridgingUnimplemented,
detail: "scalar array element depth \(elementDepth) is not a contiguous buffer")
}
return .scalarArrayMapping(element: try map(element, nullable: false, transfer: .none, context: context))
case .typeRef:
let elementMapping = try map(element, nullable: false, transfer: .none, context: context)
guard elementMapping.category == .needsClass else {
throw MapError(reason: .arrayBridgingUnimplemented,
detail: "array element type '\(elementMapping.swiftType)' is not an object or interface")
}
guard elementDepth == 1 else {
throw MapError(reason: .arrayBridgingUnimplemented,
detail: "object array element depth \(elementDepth) is not a pointer vector")
}
guard transfer == .none else {
throw MapError(reason: .arrayBridgingUnimplemented,
detail: "object array with transfer-ownership=\(transfer.rawValue)")
}
return .objectArrayMapping(element: elementMapping)
default:
throw MapError(reason: .arrayBridgingUnimplemented,
detail: "array element type is not a string, C scalar, object, or interface")
}
}
/// Pointer indirection level of a C type spelling. `gpointer` /
/// `gconstpointer` count as one level even though they carry no `*`.
private func cPointerDepth(_ cType: String) -> Int {
let stripped = cType.replacingOccurrences(of: "const", with: "")
.replacingOccurrences(of: " ", with: "")
let stars = stripped.filter { $0 == "*" }.count
let base = stripped.replacingOccurrences(of: "*", with: "")
return (base == "gpointer" || base == "gconstpointer") ? stars + 1 : stars
}
// MARK: - Internal dispatch
/// Top-level dispatch: one switch with thin per-category delegation so that

View file

@ -495,6 +495,33 @@ public struct TypeRegistry: Sendable {
return false
}
/// Checks whether a method in the given class would shadow a non-`open`
/// method on an ancestor class from a *different* Swift module. Since
/// generated methods are `public` (not `open`), Swift rejects such
/// shadowing as an implicit override attempt. Methods flagged here must be
/// skipped they cannot be emitted with `override` because the ancestor
/// lives in another module.
///
/// See ``overridesAncestorMethod(named:paramNames:in:)`` for the
/// same-module case where `override` is valid.
///
/// - Parameters:
/// - methodName: The raw GIR method name (e.g. `"set_name"`).
/// - paramNames: The non-instance parameters' raw GIR names, in order.
/// - girName: The fully qualified GIR name of the declaring class.
/// - Returns: `true` when an ancestor in a different Swift module declares
/// a method of the same signature.
public func shadowsCrossModuleAncestorMethod(named methodName: String, paramNames: [String], in girName: String) -> Bool {
guard let ownModule = resolve(girName: girName)?.swiftModule else { return false }
let signature = MethodSignature(name: methodName, paramNames: paramNames)
for ancestor in ancestry(of: girName) where ancestor.swiftModule != ownModule {
if instanceMethodSignatures[ancestor.girName]?.contains(signature) == true {
return true
}
}
return false
}
/// Whether a class descends from `GObject.InitiallyUnowned`.
///
/// This is the sole basis for deciding that a constructor's result must be

View file

@ -618,6 +618,7 @@ final class GIRXMLDelegate: NSObject, XMLParserDelegate {
builder.children.append(type)
$0 = .type(builder)
case .array(var builder):
if builder.children.isEmpty { builder.info.elementCType = cType }
builder.children.append(type)
$0 = .array(builder)
default: