现在位置: 首页 > Swift 教程 > 正文

Swift 与 Objective-C / C 互操作

Swift 从设计之初就考虑了与既有生态的共存,因此调用 Objective-C 和 C 代码几乎是零成本的。

反方向也能走通:给 Swift 的成员加上 @objc,Objective-C 就能像使用原生类一样调用它们。

与 C++ 的互操作更晚一些,需要显式打开互操作模式,而且有明确的限制。

本篇按「Swift 调用对方」和「对方调用 Swift」两个方向,把三条边界都走一遍。


三种互操作边界

先看整体,三条边界的开启方式和限制各不相同。

方向开启方式主要限制
Swift 调用 Objective-C桥接头文件或模块映射需要 Objective-C 运行时,主要在 Apple 平台
Objective-C 调用 SwiftSwift 成员加 @objc,导入生成的 -Swift.h成员类型必须能表示成 Objective-C 类型
Swift 调用 C桥接头文件或模块映射需要手动管理指针生命周期
C 调用 Swift用 @_cdecl 导出参数和返回值必须是 C 兼容类型
Swift 与 C++ 互操作打开 -cxx-interoperability-mode不支持 C++ 异常与虚函数继承

桥接头文件(bridging header)是给 App target 用的,模块映射(module map)是给框架和包用的。


Swift 调用 Objective-C

在 Xcode 工程里第一次添加 Objective-C 文件时,Xcode 会主动询问是否创建桥接头文件。

也可以在 Build Settings 的 Objective-C Bridging Header 里手动指定一个头文件路径。

桥接头文件里 #import 进来的 Objective-C 头文件,Swift 代码可以直接使用,不需要再 import。

实例

// 文件路径:RunoobGreeter.h
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface RunoobGreeter : NSObject
@property (nonatomic, copy) NSString *site;
- (NSString *)greet;
@end

NS_ASSUME_NONNULL_END

实例

// 文件路径:RunoobGreeter.m
#import "RunoobGreeter.h"

@implementation RunoobGreeter
- (instancetype)init {
    self = [super init];
    if (self) { _site = @"www.runoob.com"; }
    return self;
}
- (NSString *)greet {
    return [NSString stringWithFormat:@"Hello, %@", self.site];
}
@end

实例

// 文件路径:app.swift
import Foundation

// 桥接头文件让 Swift 直接看到 Objective-C 类
let greeter = RunoobGreeter()
greeter.site = "RUNOOB"
print(greeter.greet())
Hello, RUNOOB

用命令行验证时,对应的参数是 -import-objc-header

$ swiftc -swift-version 6 -import-objc-header RunoobGreeter.h \
    -o runoob_app app.swift RunoobGreeter.m -framework Foundation
$ ./runoob_app
Hello, RUNOOB

重要细节:头文件里的 NS_ASSUME_NONNULL_BEGINNS_ASSUME_NONNULL_END 不能省。没有它,Objective-C 的 NSString * 会被 Swift 导入成 String!,打印出来就是 Optional("Hello, RUNOOB"),容易埋下隐式解包崩溃的隐患。


Objective-C 调用 Swift:@objc 与 -Swift.h

编译器会为每个含 Swift 代码的模块生成一个 Objective-C 头文件,名字是 模块名-Swift.h

框架里通常是 产品名-Swift.h,命令行编译时由 -module-name 决定。

Objective-C 文件只要 #import "RunoobService-Swift.h",就能使用 Swift 里标记了 @objc 的成员。

实例

// 文件路径:RunoobService.swift
import Foundation

// @objcMembers:让类中所有兼容 Objective-C 的成员自动带上 @objc
@objcMembers
public class RunoobService: NSObject {
    public var site: String = "www.runoob.com"

    public func greet(_ name: String) -> String {
        "Hello, \(name), welcome to \(site)"
    }

    // 把 Swift 方法名重命名成指定的 Objective-C 选择器
    @objc(runoobVersion)
    public func version() -> String {
        "RUNOOB-1.0"
    }
}

实例

// 文件路径:main.m
#import <Foundation/Foundation.h>
#import "RunoobService-Swift.h"

int main(void) {
    @autoreleasepool {
        RunoobService *service = [[RunoobService alloc] init];
        service.site = @"RUNOOB";
        NSLog(@"%@", [service greet:@"Runoob"]);
        NSLog(@"%@", [service runoobVersion]);
    }
    return 0;
}
$ ./runoob_objc_app
Hello, Runoob, welcome to RUNOOB
RUNOOB-1.0

生成的头文件里,对应的声明长这样。

@interface RunoobService : NSObject
@property (nonatomic, copy) NSString * _Nonnull site;
- (NSString * _Nonnull)greet:(NSString * _Nonnull)name SWIFT_WARN_UNUSED_RESULT;
- (NSString * _Nonnull)runoobVersion SWIFT_WARN_UNUSED_RESULT;
- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;
@end

这里有一个特别容易踩的坑:生成的 -Swift.h 只包含 public 和 open 的成员

把上面的 public 都去掉,头文件里就什么都找不到,Objective-C 端会直接报 unknown receiver。


@objc、@objcMembers 与 dynamic

这三个关键字经常一起出现,但作用并不相同。

关键字作用典型场景
@objc把单个成员暴露给 Objective-C精确控制哪些成员可被 Objective-C 调用
@objcMembers给类中所有兼容成员自动加 @objc整个类都要被 Objective-C 使用
dynamic强制走动态派发,而不是静态或虚表派发需要 KVO、方法交换等运行时特性

@objc 后面可以跟一个括号指定选择器名,这就是前面例子里 @objc(runoobVersion) 的用法。

不指定时,Swift 会把方法名转成对应的 Objective-C 选择器,参数标签的映射规则由编译器处理。

dynamic 单独使用时表示「这个成员必须动态派发」,加上 @objc dynamic 则表示「通过 Objective-C 消息机制动态派发」,这是 KVO 和 swizzling 的前提。

注意:标了 @objc 的成员,参数和返回值的类型必须能表示成 Objective-C 类型。例如把一个 Swift 结构体当作参数传进去,编译器会直接报错:instance method cannot be marked '@objc' because the type of the parameter cannot be represented in Objective-C。


C 互操作

Swift 调用 C 和调用 Objective-C 的方式一样,通过桥接头文件或模块映射即可。

实例

// 文件路径:runoob.h
#ifndef RUNOOB_H
#define RUNOOB_H

int runoob_add(int a, int b);
int runoob_strlen(const char *s);

#endif

实例

// 文件路径:app.swift
import Foundation

// 通过桥接头文件直接调用 C 函数
print(runoob_add(20, 22))
print(runoob_strlen("www.runoob.com"))
$ swiftc -swift-version 6 -import-objc-header runoob.h -o runoob_app app.swift helper.c
$ ./runoob_app
42
14

反方向,把 Swift 函数导出给 C 使用,需要 @_cdecl 并指定一个 C 符号名。

实例

// 文件路径:lib.swift
import Foundation

// 导出成名为 runoob_add 的 C 符号
@_cdecl("runoob_add")
public func runoobAdd(_ a: Int32, _ b: Int32) -> Int32 {
    a + b
}

// 接收 C 字符串指针
@_cdecl("runoob_strlen")
public func runoobStrlen(_ s: UnsafePointer<CChar>) -> Int32 {
    Int32(strlen(s))
}

实例

// 文件路径:main.c
#include <stdio.h>
#include "runoob.h"

int main(void) {
    printf("%d\n", runoob_add(20, 22));
    printf("%d\n", runoob_strlen("www.runoob.com"));
    return 0;
}
$ swiftc -swift-version 6 -parse-as-library -o runoob_app main.c lib.swift
$ ./runoob_app
42
14

注意 -parse-as-library 不能少,否则 Swift 会把 lib.swift 当成主文件,和 main.c 的 main 函数冲突。

C 互操作的核心是 UnsafePointer 家族,它们的语义差别很大。

类型含义能否修改指向的内存
UnsafePointer<T>指向单个值的只读指针
UnsafeMutablePointer<T>指向单个值的可写指针
UnsafeBufferPointer<T>指向一段连续内存的只读缓冲区
UnsafeMutableBufferPointer<T>指向一段连续内存的可写缓冲区
UnsafeRawPointer不带类型信息的裸指针

不要自己持有这些指针,而是用 withUnsafe... 系列函数,在闭包内临时使用。

实例

import Foundation

// withUnsafePointer:临时获取某个值的内存地址
var value = 42
withUnsafePointer(to: &value) { ptr in
    print("地址可读,值为 \(ptr.pointee)")
}

// withContiguousStorageIfAvailable:把字符串的 UTF-8 字节临时暴露成缓冲区
let site = "RUNOOB"
let count = site.utf8.withContiguousStorageIfAvailable { buffer -> Int in
    buffer.count
}
print("字节数:\(count ?? -1)")

// withUnsafeBufferPointer:直接遍历数组的底层缓冲区
let numbers = [10, 20, 30]
let sum = numbers.withUnsafeBufferPointer { buffer -> Int in
    var total = 0
    for i in 0..<buffer.count {
        total += buffer[i]
    }
    return total
}
print("总和:\(sum)")
地址可读,值为 42
字节数:6
总和:60

这些函数保证指针只在闭包执行期间有效,出了闭包就不应该再使用,这样就把悬垂指针的风险降到了最低。


与 C++ 互操作

Swift 5.9 起支持直接调用 C++ 代码,需要打开互操作模式 -cxx-interoperability-mode

C++ 的类型通过模块映射暴露给 Swift,写法与 C 的模块映射类似,但需要 requires cplusplus

实例

// 文件路径:RunoobMath.hpp
#pragma once

struct RunoobCounter {
    int value = 0;
    void add(int n) { value += n; }
    int get() const { return value; }
};

inline int runoobPort() { return 8080; }

实例

// 文件路径:module.modulemap
module RunoobMath {
    header "RunoobMath.hpp"
    requires cplusplus
}

实例

// 文件路径:app.swift
import Foundation
import RunoobMath

// C++ 的 struct 在 Swift 里以值类型的形式出现
var counter = RunoobCounter()
counter.add(40)
counter.add(2)
print(counter.get())
print(runoobPort())
$ swiftc -swift-version 6 -cxx-interoperability-mode=default -I . -o runoob_cxx_app app.swift
$ ./runoob_cxx_app
42
8080

这套互操作能力很强,但边界也很清楚,有两件事做不了。

第一是不支持 C++ 异常。一个可能抛出异常的 C++ 函数在 Swift 看来就是普通函数,异常一旦跨过边界,运行时会直接终止程序。

$ ./eapp
C++ exception handling detected but the Swift runtime was compiled with exceptions disabled

第二是不支持继承 C++ 的类和重写虚函数,Swift 无法把 C++ 类当成父类。

error: inheritance from non-protocol, non-class type 'RunoobBase'

因此 C++ 互操作更适合「调用无状态或值语义的工具代码」,而不是把整个 C++ 类层次搬进 Swift。


常见问题

为什么 Objective-C 端找不到我的 Swift 类

先确认这个类是否写了 @objc@objcMembers

再确认它和它的成员是不是 publicopen,生成的 -Swift.h 只包含这两个级别的成员。

桥接头文件和模块映射该怎么选

App target 用桥接头文件,在 Build Settings 里指定路径即可。

框架、库和 Swift Package 用模块映射,因为这类产物需要把 C 头文件作为模块的一部分导出。

@objc 会带来性能损失吗

会有一点,因为调用走的是 Objective-C 消息派发,而不是 Swift 的静态派发。

只在确实需要给 Objective-C 使用或需要运行时特性时才加,不要无差别地给整个类加 @objcMembers。

withUnsafePointer 返回的指针能保存下来以后用吗

不能。它只在闭包执行期间有效,闭包返回后指针就失效了。

需要长期持有内存时,应该用 UnsafeMutablePointer.allocate 手动分配并自己负责释放。

C++ 互操作需要改 C++ 代码吗

多数情况下不需要,只要头文件能被 Clang 解析即可。

但用到模板、异常、多重继承等特性时,编译器可能跳过或简化这些声明,需要逐个验证。