C交互:直接支持

C++交互:

  • swift5.9 之前:不能直接交互,折中方案为Swift -> OC(ObjectiveC++/.mm) -> C++。 通过OC 中转提供。

  • swift5.9之后:支持直接交互,但是要做相关配置,参考官网文档

    支持C++交互配置:Xcode Build Settings:C++ and Objective-C interoperability设置为C++ / Objective-C++。 其他编译系统(比如SPM、Cocoapods)需要设置-cxx-interoperability-mode= default 需要开启, -I : path应该包含module.modulemap文件

坑一:类型系统不一致(数据映射陷阱)

指针

当与C/C++混合开发时,时常需要和指针打交道。

Swift指针与类C指针的对应关系如下:

Swift 类C 说明
unsafePointer<T> const T * 指针及所指向的内存都不可变
unsafeMutablePointer T * 指针及所指向的内存均可改变
unsafeRawPointer const void * 指针指向未知类型,指向的内存不可变
unsafeMutableRawPointer void * 指针指向未知类型,指向的内存可变
基本概念
  1. Swift中的指针分为两类,指定数据类型指针(typed pointer)未指定数据类型指针(raw pointer)
  2. 创建指针* 的方式有两种:
  • 调用withUnsafePointer(to:)方法创建
  • 通过allocate方法进行创建,但需要调用deallocate进行销毁
  1. 内存值

通过pointee可以访问存储类型值,如果是可变还可以赋值。

func incrementor(ptr: UnsafeMutablePointer<Int>) {
    ptr.pointee += 1 
}
  1. 类型绑定
  • bindMemory:将指针绑定到具体类型,用于未知类型指针去绑定类型
  • assumingMemoryBound: 假装绑定新的类型,用于某些特殊类型,例如获取结构体某个变量的指针
  • withMemoryRebound:临时更改绑定类型,用于需要指针类型类型转成其他类型使用,例如Int类型指针需要转成UInt64使用
Swift中指针操作

结构体的内存对齐

CPU 访问内存并不是一个字节一个字节读的,而是按“块 pack”(Word,字长)读取的。

  • 在 32 位系统上,通常一次读 4 字节。
  • 在 64 位系统上,通常一次读 8 字节。
    未对齐的代价 :如果一个 Int32 (4 字节)存储在地址 0x01 (横跨了两个读取块),CPU 可能需要执行 两次 内存读取,然后进行位移拼接,才能拿到这个数。
    对齐的好处 :如果它存储在 0x04 (4 的倍数),CPU 一次 就能读出来。

因此,编译器为了性能,会自动在变量之间插入无用的字节(Padding),确保每个变量的起始地址是其“对齐边界”的倍数8。

默认Padding

C和C++默认写法都会进行Padding(填充,字节对齐),一般情况对其方式一致。
C++ Padding规则示例:

// C++ 默认布局 (通常是 #pragma pack(8) 或 pack(4))
struct Packet {
    uint8_t  type;   // 占 1 byte,Offset 0
    // --- Padding ---
    // 下一个字段 length 是 uint32_t (4 bytes),它的地址必须能被 4 整除。
    // 当前地址是 1,不能被 4 整除。
    // 所以编译器会在这里插入 3 个字节的 Padding (空数据)。
    // ----------------
    uint32_t length; // 占 4 bytes,Offset 4 (1 + 3)
    
    uint8_t  flag;   // 占 1 byte,Offset 8
    
    // --- 尾部 Padding ---
    // 整个结构体的大小通常必须是最大成员对齐数 (4) 的倍数。
    // 当前大小 9 (4+4+1)。为了补齐到 4 的倍数 (12)。
    // 会再插入 3 bytes。
    // ----------------
};
// 最终 sizeof(Packet) = 12
紧凑模式

C++ 可以通过 #pragma pack(1)__attribute__((packed)) 关闭默认的 Padding,使结构体占用的内存最小(比如在网络传输时,蓝牙协议)。

// C++ 紧凑模式示例
#pragma pack(push, 1) // 强制 1 字节对齐(无 Padding)
struct PacketPacked {
    uint8_t  type;   // Offset 0
    uint32_t length; // Offset 1 (紧挨着)
    uint8_t  flag;   // Offset 5
};
#pragma pack(pop)
或者
// C 也可以使用 __attribute__((packed))
#pragma pack(1)
typedef struct __attribute__((packed)) {
    uint8_t type;              // 1 字节
    uint32_t  length;          // 4 字节
    uint8_t flag;              // 1 字节
} PacketPacked;
#pragma pack()

// 最终 sizeof(PacketPacked) = 6
Swift 怎么定义读取

Swift 的 struct 默认布局是 不确定的 (编译器可以自由重排字段以优化内存),但对于 C 互操作的 struct ,通常遵循 C 的标准对齐规则。

致命问题 :Swift 没有直接等价于 #pragma pack(1) 的语法糖(所以,如果是紧凑型,Swift不能直接进行内存映射,会导致数据错位或者crash)
一般来说我们会直接定义

struct SwiftPacket {
    var type: UInt8
    var length: UInt32
    var flag: UInt8
}

Swift 编译器几乎肯定会按照 默认对齐 处理:

  • MemoryLayout.size = 9 (实际占用字节)

  • MemoryLayout.stride = 12 (数组中步长,含尾部 Padding)

  • MemoryLayout.alignment = 4
    后果 :

  • 如果你从 C++ 收到一个 6 字节的二进制流(紧凑包),直接用 memcpy 或 Data.withUnsafeBytes 强转成 SwiftPacket 。

  • type (Offset 0) 读取正确。

  • length (Swift 期望在 Offset 4) 读取的是 C++ 数据流中 Offset 4 的位置。 但在 C++ 紧凑包里,Offset 4 已经是 length 的最后一个字节了!

  • 结果:数据完全读错,解析出巨大的随机数

方案1: 手动序列化

不要把 Swift 结构体当做内存映射。把它当做纯粹的模型,手动从 Data 中逐个字节读取。最安全

struct Packet {
    let type: UInt8
    let length: UInt32
    let flag: UInt8
    
    // 从二进制数据初始化
    init?(data: Data) {
        // 假设 C++ 是 pack(1),总长 6 // 注意如果是Padding,这里会出错
        guard data.count >= 6 else { return nil }
        
        // 1. 读 type (offset 0)
        self.type = data[0]
        
        // 2. 读 length (offset 1, 4 bytes)
        // 注意:loadUnaligned 非常重要,因为 offset 1 不是 4 的倍数 // loadUnaligned:原样取对应的字节(适合没有对齐字节),
        let lengthData = data[1..<5] // 获取数据切片
        //self.length = lengthData.withUnsafeBytes { $0.loadUnaligned(as: UInt32.self) } // 类型转换(二进制 -> 整数),默认是小端序,如果数据是小端
            // 读出来后按照大端序标记(蓝牙协议通常是大端),然后再转整数。
         self.length = lengthData.withUnsafeBytes { $0.loadUnaligned(as: UInt32.self).bigEndian }
    
        
        // 3. 读 flag (offset 5)
        self.flag = data[5]
    }
}
方案2: 使用 Clang 导入 (最省事)

如果这是一个混编项目, 不要在 Swift 里重新定义结构体 。
直接在 C/C++ Header (.h) 中定义结构体,并在 Bridging-Header 中引入

// Packet.h
struct __attribute__((packed)) Packet {
    uint8_t type;
    uint32_t length;
    uint8_t flag;
};

Swift 会自动识别这个 C 结构体,并生成对应的 Swift 版本,且 自动保留 其紧凑布局属性。
你在 Swift 里直接使用 Packet 类型即可,它是安全的。

在Swift端如果进行类型定义,需要关注C/C++端是否使用了紧凑型。如果是需要手动解析。

坑二:C++ 对象模型的生命周期

  1. 对象指针的传递

    • 坑点:Swift 看不到 C++ 的类定义,只能把 C++ 对象当作 UnsafeRawPointer (OpaquePointer) 传递。
    • 体验:在 Swift 代码里像是手撸 C 语言,毫无类型感。
    • 经验:建立 Swift 的 Wrapper Class,在其 deinit 中调用 C++ 的析构函数。
    // C++ 接口 (C-shim)
    // void* create_engine();
    // void destroy_engine(void* engine);
    // void engine_start(void* engine);
    
    // Swift Wrapper (RAII)
    final class EngineWrapper {
        private let handle: UnsafeMutableRawPointer
        
        init() {
            self.handle = create_engine()
        }
        
        deinit {
            // 确保对象释放,防止内存泄漏
            destroy_engine(self.handle)
        }
        
        func start() {
            engine_start(self.handle)
        }
    }
    
    // 使用时,ARC 自动管理生命周期
    do {
        let engine = EngineWrapper()
        engine.start()
    } // 离开作用域,engine 销毁 -> deinit -> destroy_engine
    
  2. 所有权归属

    • 坑点:对象是在 C++ 创建由 Swift 释放?还是反之?双重释放 闪退。
    • 经验:明确注释所有权(RAII 思想),在 Swift 端使用 class 包装指针,利用 ARC 自动管理 C++ 对象的生命周期。
    • 高级技巧:如果是 Swift 5.9+ C++ Interop,Swift 可以直接导入 C++ 类(Value Type 或 Reference Type)。
      • 对于 Foreign Reference Type (引用类型),Swift 自动管理引用计数(需在 C++ 侧实现 retain/release)。
      • 对于 Value Type (值类型),Swift 会自动调用 C++ 的 Copy Constructor 和 Destructor。

坑三:字符串与数组(数据转换)

在 Swift 与 C++ 交互时,字符串和数组是最频繁交换的数据类型。由于两者的内存模型和编码方式完全不同,稍有不慎就会导致乱码、Crash 或性能瓶颈。

1. String 转换:编码与指针生命周期的博弈

  • 坑点
    • 编码差异:C/C++ 常用 char* (UTF-8) 或 wchar_t*,Swift 原生是 Unicode。转换时如果未指定编码,中文或特殊符号会变成乱码。
    • 指针悬垂:Swift 的 String.cString 属性返回的是一个临时的 UnsafePointer<CChar>,如果将其保存起来稍后使用,指针指向的内存很可能已经被释放,导致 Crash。
  • 经验:使用 withCStringwithCString(repairedUnpairedSurrogates:) 来确保指针在闭包执行期间有效;对于 C++ 的 std::string,利用 Swift 5.9 的互操作特性或手动进行 UTF-8 转换。

代码示例:Swift String -> C++ std::string (或 const char\*)

import Foundation

// 假设 C++ 函数声明: void cppProcessName(const char* name);

let swiftUserName = "张三@Dev"

// ✅ 正确做法:使用 withCString
// Swift 会保证在闭包执行期间,cString 指针是有效的
swiftUserName.withCString { cStringPtr in
    // 此时 cStringPtr 类型是 UnsafePointer<CChar> (即 const char*)
    cppProcessName(cStringPtr)
}

// ❌ 错误做法:直接保存指针
// let dangerousPtr = swiftUserName.cString(using: .utf8)!
// 延迟调用 cppProcessName(dangerousPtr) -> 可能 Crash,因为内存已回收

代码示例:C++ std::string -> Swift String

// 假设 C++ 函数声明: std::string cppGetVersion();

// ✅ 如果使用了 Swift 5.9 C++ Interop,直接调用
// let version: String = cppGetVersion() // 自动转换,Swift 会根据 UTF-8 解码

// ✅ 手动互操作(C 风格):
// 假设返回的是 const char*,且由 C++ 维持生命周期
let cVersionPtr = cppGetVersionPtr() 
if let ptr = cVersionPtr {
    // 从指针创建 Swift String,指定编码为 UTF-8
    let swiftVersion = String(cString: ptr)
    print(swiftVersion)
}

2. 数组/Buffer 传递:内存连续性与零拷贝

  • 坑点
    • 内存连续性假象:虽然 Swift 的 Array 当前实现通常是连续内存,但 API 并不保证这一点。直接取地址传给 C++ 是不安全的。
    • 性能杀手:如果在高频调用(如音视频帧处理)中,将 Swift Array 拷贝给 C++ std::vector,或者把 C++ Buffer 拷贝给 Swift Array,会造成巨大的 CPU 开销和内存颠簸。
  • 经验:使用 withUnsafeBufferPointer 获取稳定的内存地址;在 C++ 接口设计上,尽量使用 指针 + 长度 的方式,而不是直接传递 std::vector 对象。

代码示例:Swift Array -> C++ Buffer (高性能入参)

// 场景:发送音视频 PCM 数据
// 假设 C++ 函数声明: void cppAudioPlay(const int16_t* buffer, int size);

let pcmData: [Int16] = [100, 200, 300, 400, ...] // 假设有大量数据

// ✅ 正确做法:零拷贝传递
// withUnsafeBytes 直接访问底层内存,不发生数据复制
pcmData.withUnsafeBytes { rawBufferPtr in
    // rawBufferPtr.baseAddress 指向数据的真实内存地址
    // 转换为 C++ 需要的指针类型 (UnsafePointer<Int16>)
    if let baseAddr = rawBufferPtr.baseAddress?.assumingMemoryBound(to: Int16.self) {
        cppAudioPlay(baseAddr, pcmData.count)
    }
}

// ❌ 错误做法:循环拷贝
// for i in 0..<pcmData.count { vector.push_back(pcmData[i]) } // 极慢

代码示例:C++ Buffer -> Swift Array (处理出参)

// 场景:C++ 填充数据给 Swift
// 假设 C++ 函数声明: void cppGetImageData(uint8_t** outData, int* outSize);

var dataPtr: UnsafeMutablePointer<UInt8>?
var size: Int32 = 0

// 调用 C++ 获取数据
cppGetImageData(&dataPtr, &size)

// ✅ 正确做法:根据指针初始化 Swift Array(浅拷贝或深拷贝取决于类型)
if let ptr = dataPtr, size > 0 {
    // 创建一个 BufferPointer 指向 C++ 的内存
    let buffer = UnsafeBufferPointer(start: ptr, count: Int(size))
    
    // 初始化 Swift Array (此时会进行一次内存复制,数据归 Swift 所有,安全)
    let swiftImage = Array(buffer)
    
    // 注意:如果 C++ 侧要求由调用者释放内存,这里还需要调用 C++ 的释放函数
    // cppFreeImageData(dataPtr)
    
    print("Received \(swiftImage.count) bytes")
}

总结建议

  • String:除非必要,不要在 C++ 和 Swift 之间频繁传递字符串。如果必须传,尽量用 const char* + UTF-8。
  • Array入参withUnsafeBytes(零拷贝);出参UnsafeBufferPointer 初始化 Array。避免直接跨语言传递 std::vector 和 Swift Array 的对象本身。

坑四:回调与闭包的“断桥”(函数指针与 Block)

假设 C++(或 C)侧有一个耗时的下载任务,它需要回调进度给 Swift。

C++ 侧接口定义 (C 风格导出):

// DownloadManager.h

// 定义回调函数类型:包含进度和一个泛型上下文指针
typedef void (*OnProgressCallback)(int percent, void* context);

// 开始下载,传入回调函数和上下文
extern "C" void startDownload(OnProgressCallback callback, void* context);

// 停止下载(用于演示资源释放)
extern "C" void stopDownload();
2. Swift 侧实现

核心难点: Swift 的闭包无法直接变成 OnProgressCallback,因为闭包捕获了 self,而函数指针只是一个内存地址,无法携带 self

解决方案: 使用 Unmanaged 手动管理内存引用,配合静态函数转发。

import Foundation

class Downloader: NSObject {
    
    // 标记当前是否持有 C++ 的回调上下文
    private var isListening = false
    
    func startTask() {
        // 1. 准备 Context
        // 使用 Unmanaged.passRetained(self) 将当前对象转换为 void* 指针
        // passRetained 会增加引用计数,防止 Swift 对象在 C++ 异步回调回来前被释放
        let contextPtr = Unmanaged.passRetained(self).toOpaque()
        
        // 2. 传入静态函数和 context
        // staticProgressCallback 是全局静态函数,符合 C 函数指针签名
        startDownload(staticProgressCallback, contextPtr)
        
        isListening = true
    }
    
    func stopTask() {
        stopDownload()
        isListening = false
        // 注意:实际项目中,stop 时可能需要配合 C++ 侧通知来 balance passRetained
    }
    
    // MARK: - 实际业务逻辑(Swift 闭包环境)
    private func handleProgress(percent: Int32) {
        print("🚀 下载进度: \(percent)%")
        // 这里可以安全地使用 self,更新 UI 等
    }
    
    // MARK: - Trampoline(蹦床函数)
    // 这个函数是静态的,没有捕获上下文,可以转换为 C 函数指针
    private static func staticProgressCallback(percent: Int32, context: UnsafeMutableRawPointer?) {
        // 1. 从 void* 指针还原回 Swift 对象
        // fromOpaque: 将指针还原回 Unmanaged
        // takeUnretainedValue: 获取对象,但不改变引用计数(因为 C++ 侧不持有所有权,仅借用)
        guard let context = context else { return }
        let downloader = Unmanaged<Downloader>.fromOpaque(context).takeUnretainedValue()
        
        // 2. 调用实例方法
        downloader.handleProgress(percent: percent)
        
        // 高级场景(可选):
        // 如果这是“一次性”回调(例如下载完成),这里应该调用 passRetained 后的 release
        // Unmanaged<Downloader>.fromOpaque(context).release()
    }
    
    deinit {
        print("Downloader 释放")
    }
}
3. 代码详解与避坑指南
  1. 为什么必须是 static 函数?
    • C 函数指针只对应一段代码的地址。Swift 的实例方法(如 func handleProgress)底层实际上隐含了 self 参数(类似 func handleProgress(self: Downloader, percent: Int)),这与 C 定义的 (int, void*) 签名不匹配。
    • static 函数不依赖实例,签名完全匹配 C 的定义。
  2. Unmanaged 的魔力:
    • Swift 的 ARC 不认识 C++ 的 void*。如果你直接传 &self,一旦 startTask 执行完毕,Swift 可能会因为 self 没有被强引用而销毁对象。稍后 C++ 回调时,context 就成了悬垂指针,App 必崩。
    • Unmanaged.passRetained(self):告诉 ARC “这个 C++ 指针正在持有我,不要回收我”,同时返回指针地址。
  3. 内存平衡(Balance):
    • 在上面的例子中,如果 Downloader 是长期存活的(比如属于 ViewController),只要在 deinitstop 时停止 C++ 的回调即可,passRetained 增加的计数会在对象销毁时自动归零。
    • 特殊场景(一次性回调): 如果 C++ 的回调只触发一次(如 void onComplete(void* ctx)),且触发后 C++ 会自动清空指针,那么在 Swift 的 static 回调函数里,必须手动调用 Unmanaged.fromOpaque(context).release(),否则会造成内存泄漏(Reference Count 永远减不回 0)。
4. 如果使用 Swift 5.9 C++ Interop?

Swift 5.9+ 使得互操作更简单,但如果你的 C++ 回调依然使用传统的函数指针(而非 std::function),上述 Trampoline 模式依然是标准解法。

如果 C++ 侧使用的是 std::function

  • C++ Interop 会将其映射为 Swift 的闭包。
  • 你可以直接传 Swift 闭包进去:startDownload { percent in ... }
  • 但前提是 C++ 侧必须接受 std::function,这是 C++11 的特性,旧代码库通常还是用的函数指针。
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐