C++如何提供跨平台的回调函数接口是业务开发很常遇到的需求,但是实际上公开的资料都很杂乱,而且不一定正确,因此在此文章中,以Android Java和iOS Swift为例,系统化说明一下如何制作一个跨平台C++回调函数接口。
对于C++而言,最常用的建构回调函数方法是基于std::function
std::function
是 C++ 标准库中的一个模板类,用于封装函数对象(Function Object)、函数指针、以及可以调用的(如 lambda 表达式)。它提了一种通用的方式来存储、传递和调用各种类型的可调用对象。
std::function
的主要作用是将可调用对象视为一个函数类型,并提供了一组成员函数来操作和调用这个函数对象。它的声明如下:template
class function; 其中
Signature
是函数类型的签名,包括返回类型和参数类型。
std::function
的使用非常灵活,可以用于存储各种类型的可调用对象,包括函数指针、成员函数指针、函数对象、以及 lambda 表达式等。可以通过以下方式来创建std::function
对象:std::function
func; // 声明一个接受 int 类型参数并返回 int 类型的函数对象 // 将一个 lambda 表达式赋值给 func func = [](int x) { return x * 2; }; // 调用 func int result = func(5); // 调用 lambda 表达式,得到结果 10
std::function
还可以作为函数参数或返回值使用,以实现更灵活的函数接口。总而言之,
std::function
提供了一种通用的封装方式,使得各种可调用对象可以以统一的方式进行存储、传递和调用,提高了代码的灵活性和可重用性。它在函数回调、事件处理等场景下非常有用。
因此制作跨平台的回调函数接口也是围绕std::function展开,主要其实就分成两的步骤
1. 使用std::function制作C++ 回调函数接口,以及回调函数触发的代码
2. 使用目标平台的语言实现方法转换,将目标平台的语言的方法转换成std::function,本文章会介绍使用Android Java和iOS Swift实现转换的代码
首先在C++声明一个回调函数
std::function callback_ = nullptr;
然后制作一个回调函数注册接口
void registCallback( std::function callback){
callback_ = callback;
}
最后在代码中加入触发回调的代码
int func(std::string tag){
for (int i=0; i<100 ; i++){
callback_(tag, i);
}
}
需要在jni层建构一个Java和C++函数的桥接,首先我们建构的一个Java回调函数注册的接口
jobject jCallback = nullptr;
jmethodID jCallbackMethId = nullptr;
JavaVM *g_pJVM = nullptr
std::mutex g_CallbackMutex;
extern "C" JNIEXPORT int JNICALL
Java_com_test_registerCallback(JNIEnv *env,jclass clazz,object call_back) {
// 线程保护
std::unique_lock lck(g_CallbackMutex);
// 避免重复注册
if (jCallback) {
return -1;
}
jCallback = env->NewGlobalRef(call_back);
jclass callbackClazz = env->GetObjectClass(call_back);
jCallbackMethId =
env->GetMethodID(callbackClazz, "onUpdateCallback", "(Ljava/lang/String;I)V");
env->GetJavaVM(&g_pJVM);
return 0;
}
具体分成几个步骤,首先是为android端的函数建构一个Reference,这个Reference是Java跟C++通用的地址, 其中callback就是从Java层传入的回调实现
jCallback = env->NewGlobalRef(call_back);
jclass callbackClazz = env->GetObjectClass(call_back);
接著是找到具体方法的id,这里需要名称跟入参出参的类型都正确无误的对应,否则会出现地址错误,详细的说明可以参考这里
jCallbackMethId =
env->GetMethodID(callbackClazz, "onUpdateCallback", "(Ljava/lang/String;I)V");
我们在上面已经建构了一个名叫onUpdateCallback的Java 方法,接著我们需要把这个这个Java方法封装成C++ 方法,并在调用这个C++方法时调用Java方法
void onUpdateCallback(std::string tag, in p) {
JNIEnv *pNewEnv = nullptr;
if (jCallback && jCallbackMethId && g_pJVM) {
if (g_pProgressJVM->AttachCurrentThread(
reinterpret_cast(&pNewEnv), NULL) < 0) {
return;
}
// std::string 转 jstring
jstring js = pNewEnv->NewStringUTF(key.c_str());
// 调用Java方法:onUpdateCallback
pNewEnv->CallVoidMethod(jPCallback, jCallbackMethId,
js, p);
}
}
接著在jni层找个合适的地方将建构好onUpdateCallback的Java注册到C++就行,
// 某个方法内部,可以是初始化函数,但需要避免会重复调用的方法
...
{
int ret = registCallback(onProgressCallback);
}
...
回到刚刚在jni建构了一个注册回调函数的接口
Java_com_test_registerCallback(JNIEnv *env,jclass clazz,object call_back) {
这里的object call_back就是我们在Java层实现的回调函数本体,但是因为需要有固定的地址,因此需要做一个interface的封装,确保传入时候方法已经实例化,而且地址唯一
interface的声明如下
public static interface ICallback{
void onCallback(String k, float p);
};
接著在registerCallback声明的地方将interface做为入参
public static native int registerCallback(IProgressCallback callBack);
到此为止,回调函数的封装已经全数完成了,可以在业务层实现一个简单的回调函数测试一下
testSdk.registerCallback(new testSdk.ICallback() {
@Override
public void onCallback(String k, float p) {
Log.e("[onUpdateCallback][" + k + "] p: " + p);
});
}
});
一般C++跟Swift交互会需要一个Object C的桥接层,而且C++本身跟ObjectC兼容起来相对容易很多,因此建议就从Object C的桥接层著手
// bridge.h
// 定义callback function 类型
#ifdef __cplusplus
extern "C" {
#endif
typedef void(*callback_t)(const char * k, float p);
#ifdef __cplusplus
} // extern "C"
#endif
// 注册回调函数
- (int) registCallback: (callback_t) callback;
根据C++ 声明的回调函数声明一个type,并将这个type当成注册函数registCallback的入参
注册函数的实现相对于Android相对单纯,直接调用C++层注册函数即可
// bridge.mm
- (int) setProgressCallback: (callback_t) callback{
// 调用C++层注册函数
int ret = test_sdk->setCallback(callback);
return ret;
}
以下是在Swift中实现一个简单的回调函数示例
test_sdk.setPCallback({(value1, value2) -> Void in
if let pointer = value1 {
let swiftString = String(cString: pointer)
print("p[\(swiftString)]:", value2);
} else {
print("CallBack Return None")
}
})
本文章将两种不同平台的方法传入C++层做为回调函数的实现,其实可以发现C++层是不需要进行更动的,主要是需要在桥接层进行函数转换,安卓的部分在JNI实现,iOS则是在ObjectC Bridge实现。目的其实也是相同的,就是将一个Java/Swift方法实例化,并且具有固定的调用地址,提供给C++层进行调用。