第三章:创建第一个跨平台SDK项目:C++核心代码 + 各平台包装示例
好了,理论铺垫完毕。这一章我们直接动手,搭建一个真实的跨平台SDK项目骨架。我个人习惯把这类项目叫做“三明治”结构——中间是厚厚的C++核心,两边是平台特定的包装层。你想想看,这其实是性价比最高的做法:核心逻辑一次编写,平台适配各自为战。
3.1 项目结构总览:一个三明治架构
我们先看这个项目的目录长什么样。这是我多次实践中总结出来的布局,干净,且容易维护。
CrossPlatformSDK/
├── core/ # C++ 核心代码
│ ├── include/
│ │ └── math_engine.h
│ ├── src/
│ │ └── math_engine.cpp
│ └── CMakeLists.txt
├── platforms/
│ ├── ios/ # iOS Framework 包装
│ │ ├── MSMathEngine.h
│ │ ├── MSMathEngine.mm
│ │ └── Info.plist
│ ├── android/ # Android AAR 包装
│ │ ├── src/main/java/
│ │ │ └── com/example/
│ │ │ └── MathEngine.java
│ │ ├── src/main/jni/
│ │ │ └── native_bridge.cpp
│ │ └── build.gradle
│ └── desktop/ # 桌面 DLL/SO 包装
│ ├── include/
│ │ └── math_engine_export.h
│ └── src/
│ └── desktop_bridge.cpp
├── tests/ # 单元测试
│ ├── test_core.cpp
│ └── test_integration.cpp
└── scripts/ # 构建脚本
└── build_all.sh
这个结构的核心原则是什么?C++ 核心完全不感知它最终会运行在哪个平台上。它只管算数、处理数据。而所有平台相关的调用(比如 iOS 的 ObjC Runtime、Android 的 JNI 调用、Windows 的 DLL 导出)都被隔离在 platforms/ 目录下。我在项目中遇到过最痛苦的事情,就是核心代码里混入了 #ifdef WIN32 和 __ANDROID__ 宏,结果导致新平台移植时几乎要重写。所以,别这么干。
3.2 C++ 核心:纯计算逻辑的实现
我们以一个小巧但能说明问题的数学引擎为例。它只做一件事:浮点数混合运算(加法、乘法、取模优化)。
头文件:math_engine.h
#ifndef MATH_ENGINE_H
#define MATH_ENGINE_H
#include <vector>
#include <cstdint>
namespace mscore {
class MathEngine {
public:
MathEngine();
~MathEngine();
// 标准加法
double add(double a, double b);
// 带精度的乘法
double multiply(double a, double b, int decimal_places = 2);
// 快速取模(仅限正数)
int32_t fast_mod(int32_t a, int32_t b);
// 批量运算
std::vector<double> batch_add(const std::vector<double>& inputs, double increment);
private:
bool initialized_;
};
} // namespace mscore
#endif
实现文件:math_engine.cpp
#include "math_engine.h"
#include <cmath>
#include <algorithm>
namespace mscore {
MathEngine::MathEngine() : initialized_(false) {
// 嗯,这里可以做些预计算或者内存池分配
initialized_ = true;
}
MathEngine::~MathEngine() {
// 清理工作
}
double MathEngine::add(double a, double b) {
return a + b;
}
double MathEngine::multiply(double a, double b, int decimal_places) {
double factor = std::pow(10.0, decimal_places);
return std::round((a * b) * factor) / factor;
}
int32_t MathEngine::fast_mod(int32_t a, int32_t b) {
// 对于正数,直接用位运算优化
// 我曾经踩过一个坑:b=0 会导致除零异常
if (b == 0) {
return a; // 安全返回
}
return a % b;
}
std::vector<double> MathEngine::batch_add(const std::vector<double>& inputs, double increment) {
std::vector<double> result;
result.reserve(inputs.size());
for (const auto& val : inputs) {
result.push_back(val + increment);
}
return result;
}
} // namespace mscore
3.3 iOS Framework 包装:ObjC 与 C++ 的混编技巧
iOS 端需要将 C++ 类封装成一个 ObjC 类,暴露给 Swift 和 ObjC 调用。这层是关键:不要直接用 C++ 头文件暴露给上层应用,因为 Swift 无法直接处理 C++ 类。
头文件:MSMathEngine.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface MSMathEngine : NSObject
- (instancetype)init;
- (double)add:(double)a to:(double)b;
- (double)multiply:(double)a with:(double)b decimalPlaces:(int)places;
- (int32_t)fastMod:(int32_t)a by:(int32_t)b;
@end
NS_ASSUME_NONNULL_END
实现文件:MSMathEngine.mm
注意后缀 .mm,这是 ObjC++ 文件,允许你在 ObjC 中直接包含 C++ 头文件。
#import "MSMathEngine.h"
#include "math_engine.h"
@interface MSMathEngine ()
@property (nonatomic, assign) mscore::MathEngine* engine;
@end
@implementation MSMathEngine
- (instancetype)init {
self = [super init];
if (self) {
// 在堆上创建 C++ 对象
_engine = new mscore::MathEngine();
}
return self;
}
- (void)dealloc {
// 手动管理生命周期
delete _engine;
// 不需要调用 [super dealloc](ARC 环境)
}
- (double)add:(double)a to:(double)b {
return _engine->add(a, b);
}
- (double)multiply:(double)a with:(double)b decimalPlaces:(int)places {
return _engine->multiply(a, b, places);
}
- (int32_t)fastMod:(int32_t)a by:(int32_t)b {
return _engine->fast_mod(a, b);
}
@end
这里有个关键的避坑指南:我曾经在项目中忘记写 dealloc 里的 delete _engine,结果运行几天后应用突然闪退——内存泄漏了。ObjC++ 编译器不会替你管理 C++ 堆对象,你必须手动处理。
3.4 Android AAR 包装:JNI 桥接的“脏活累活”
Android 端通过 JNI(Java Native Interface)调用 C++。这层包装通常是最繁琐的,因为你要处理 JNI 类型转换和函数注册。
Java 类:MathEngine.java
package com.example;
public class MathEngine {
static {
// 加载原生库,库名对应 CMake 构建的产物
System.loadLibrary("math_engine_jni");
}
// 声明 JNI 方法
public native double nativeAdd(double a, double b);
public native double nativeMultiply(double a, double b, int decimalPlaces);
public native int nativeFastMod(int a, int b);
private long nativeHandle; // 指向 C++ 对象的指针(存储为 Long)
public MathEngine() {
nativeHandle = nativeCreate();
}
// 在 finalize 中释放 C++ 对象
@Override
protected void finalize() throws Throwable {
if (nativeHandle != 0) {
nativeDestroy(nativeHandle);
}
super.finalize();
}
// 包装方法,供应用层调用
public double add(double a, double b) {
return nativeAdd(a, b);
}
public double multiply(double a, double b, int decimalPlaces) {
return nativeMultiply(a, b, decimalPlaces);
}
public int fastMod(int a, int b) {
return nativeFastMod(a, b);
}
// JNI 辅助方法
private static native long nativeCreate();
private static native void nativeDestroy(long handle);
}
JNI 桥接层:native_bridge.cpp
#include <jni.h>
#include "math_engine.h"
extern "C" {
JNIEXPORT jlong JNICALL
Java_com_example_MathEngine_nativeCreate(JNIEnv *env, jclass clazz) {
return reinterpret_cast<jlong>(new mscore::MathEngine());
}
JNIEXPORT void JNICALL
Java_com_example_MathEngine_nativeDestroy(JNIEnv *env, jclass clazz, jlong handle) {
delete reinterpret_cast<mscore::MathEngine*>(handle);
}
JNIEXPORT jdouble JNICALL
Java_com_example_MathEngine_nativeAdd(JNIEnv *env, jobject thiz, jdouble a, jdouble b) {
mscore::MathEngine* engine = reinterpret_cast<mscore::MathEngine*>(/* 需要从实例获取 handle */);
// 实际项目中,应从 Java 对象的 long 字段中读取 handle
return engine->add(a, b);
}
} // extern "C"
reinterpret_cast 的使用非常危险。务必保证 Java 层传过来的 long 值确实指向一个合法对象,且在销毁后及时置零。我曾经遇到过一个 bug:由于 Java 垃圾回收顺序不确定,导致 JNI 侧访问了已经被 delete 的内存。3.5 桌面 DLL/SO 包装:C 接口导出
桌面端(Windows/Linux/macOS)相对简单。我们只需要用 extern "C" 和 __declspec(dllexport) 导出 C 风格函数。
导出头文件:math_engine_export.h
#ifdef _WIN32
#define MATH_API __declspec(dllexport)
#else
#define MATH_API __attribute__((visibility("default")))
#endif
extern "C" {
MATH_API void* math_engine_create();
MATH_API void math_engine_destroy(void* engine);
MATH_API double math_engine_add(void* engine, double a, double b);
MATH_API double math_engine_multiply(void* engine, double a, double b, int decimal_places);
MATH_API int32_t math_engine_fast_mod(void* engine, int32_t a, int32_t b);
}
实现文件:desktop_bridge.cpp
#include "math_engine_export.h"
#include "math_engine.h"
void* math_engine_create() {
return new mscore::MathEngine();
}
void math_engine_destroy(void* engine) {
delete static_cast<mscore::MathEngine*>(engine);
}
double math_engine_add(void* engine, double a, double b) {
return static_cast<mscore::MathEngine*>(engine)->add(a, b);
}
// ... 其余函数实现类似
桌面端包装的优点是简单直接。任何支持 C ABI 的语言(Python Ctypes、C# P/Invoke、Go Cgo)都可以调用。我个人习惯在导出时统一使用 void* 句柄,避免暴露 C++ 类型信息。
3.6 核心流程一览:从源码到平台产物
下面这张图总结了整个构建与包装的流程。说实话,这个流程我调试了无数次才定型。
3.7 构建配置与对比
不同平台的构建配置差异很大。下表总结了关键差异,方便你快速对比。
| 维度 | iOS Framework | Android AAR | 桌面 DLL/SO |
|---|---|---|---|
| 编译器 | Apple Clang (Xcode) | NDK Clang / GCC | MSVC / GCC / Clang |
| 接口形式 | ObjC Header (.h) | JNI 方法注册 | extern "C" 导出 |
| 静态/动态 | .framework (动态库包) | .aar (包含 .so) | .dll / .so / .dylib |
| 内存管理 | ARC + 手动 delete | JVM GC + 手动释放 | 完全手动管理 |
| 调试难度 | 中等(可直接断点 .mm) | 高(需要混合调试JNI) | 低(标准 native 调试) |
int、double、std::vector 等基础类型),从而减少各平台的转换逻辑。
3.8 常见踩坑与应对
- C++ 异常跨平台边界:C++ 异常不要抛出到 ObjC 或 Java 层。我曾经在 iOS 端忘了 catch,结果 ObjC 直接调用
std::terminate(),应用闪退无日志。所有平台包装层都应该用 try-catch 把 C++ 异常转为平台错误码或 NSError。 - 符号可见性:Android NDK 默认隐藏所有符号,需要显式
__attribute__((visibility("default")))。Windows DLL 则要用__declspec(dllexport)。这个细节很容易忘。 - JNI 线程模型:JNI 回调可能发生在任意线程。C++ 核心如果假定单线程,一定要在桥接层加锁或调度。