djinni是Dropbox开源的一个库,它会根据IDL定义生成相应的接口和很多胶水代码,可以做到Mac、PC、iOS、Android四个端共享接口和C++基础代码。
本文仅从iOS的角度分析一下怎么djinni的基础使用方法。djinni的调用路径是C++->ObjectiveC++->Objective-C,所以从每种语言的层面做一些分析。
整个的关系图如下所示。调用路径是WLALoginSDK->cppRef(WLALoginSDKImpl)->djinni_generated::LoginProvider->ObjcRef(WLALoginProviderImpl)
。如果未来有C++实现的OAuth 2.0客户端,调用逻辑可以做很多简化,不需要一个provider去调用AppAuth库。
脚本
从脚本可以看出C++代码的namespace是wla_gen
,而Objective-C++代码的namespace是djinni_generated
。Objective-C相关的类和协议都会加上objc-type-prefix
前缀,也就是脚本里面配置的WLA
。记住这些规律,有利于分析每个类的角色。
../djinni_exe/run \
--java-out ./app/src/main/java/com/aliyun/wla/login \
--java-package com.aliyun.wla.login \
--java-cpp-exception java.lang.RuntimeException \
--ident-java-field mFooBar \
--cpp-out ./djinni_gen_cpp \
--cpp-namespace wla_gen \
--cpp-libexport WLA_LOGIN_EXPORT \
--jni-out ./djinni_gen_jni \
--ident-jni-class NativeFooBar \
--ident-jni-file NativeFooBar \
--objc-out ./djinni_gen_oc \
--objc-type-prefix WLA \
--objcpp-out ./djinni_gen_oc \
--objcpp-namespace djinni_generated \
--idl ./all.djinni
IDL
一份IDL定义如下所示。其中LoginSDK的具体逻辑要以C++实现,向Objective-C暴露一个WLALoginSDK
类,但是它主要调用LoginProvider干活。LoginProvider通过C++定义了接口,但是通过WLALoginProviderImpl这个Objective-C类来实现具体的逻辑,比如OAuth的认证流程采用OpenID下面的AppAuth-iOS和AppAuth-Android来完成,这些库并不是C++写的。
//+c 表示用C++实现逻辑,暴露给Objective-C使用。
//+c 会在Objective-C生成一个 @interface WLALoginSDK。
LoginSDK = interface +c {
static sharedInstance(): LoginSDK;
setLoginProvider(provider: LoginProvider);
setClientID(client_id: string);
}
# wrapper of each platform's OAuth SDK
// +o +j 表示要用各端的代码实现逻辑
// 会在Objective-C生成一个 @protocol WLALoginProvider,需要有一个Objective-C实现类。
LoginProvider = interface +o +j {
setClientID(client_id: string);
}
C++
不管+c
还是+o +j
,都会生成一个C++纯虚类。
//LoginSDK.hpp
namespace wla_gen {
class WLA_LOGIN_EXPORT LoginSDK {
public:
virtual ~LoginSDK() {}
static std::shared_ptr sharedInstance();
virtual void setLoginProvider(const std::shared_ptr & provider) = 0;
virtual void setClientID(const std::string & client_id) = 0;
};
} // namespace wla_gen
//LoginProvider.hpp
namespace wla_gen {
class LoginProvider {
public:
virtual ~LoginProvider() {}
virtual void setClientID(const std::string & client_id) = 0;
};
} // namespace wla_gen
Objective-C++
不管+c
还是+o +j
,都会生成一个Objective-C++的类,文件名是xxx+private.h
和xxx+private.mm
。Objective-C++类有两个主要的作用就是toCpp和fromCpp,让C++对象和Objective-C对象可以互相找到彼此。针对+c
和+o +j
生成的代码有所不同。下面分两种情况分析一下。
+c
主要是完善WLALoginSDK类,它持有一个::wla_gen::LoginSDK
纯虚类的实现,干活都是这个cppRef
。
namespace djinni_generated {
class LoginSDK
{
public:
using CppType = std::shared_ptr<::wla_gen::LoginSDK>;
using CppOptType = std::shared_ptr<::wla_gen::LoginSDK>;
using ObjcType = WLALoginSDK*;
using Boxed = LoginSDK;
static CppType toCpp(ObjcType objc);
static ObjcType fromCppOpt(const CppOptType& cpp);
static ObjcType fromCpp(const CppType& cpp) { return fromCppOpt(cpp); }
private:
class ObjcProxy;
};
} // namespace djinni_generated
@interface WLALoginSDK ()
- (id)initWithCpp:(const std::shared_ptr<::wla_gen::LoginSDK>&)cppRef;
@end
@implementation WLALoginSDK {
::djinni::CppProxyCache::Handle> _cppRefHandle;
}
- (id)initWithCpp:(const std::shared_ptr<::wla_gen::LoginSDK>&)cppRef
{
if (self = [super init]) {
_cppRefHandle.assign(cppRef);
}
return self;
}
+ (nullable WLALoginSDK *)sharedInstance {
try {
auto objcpp_result_ = ::wla_gen::LoginSDK::sharedInstance();
return ::djinni_generated::LoginSDK::fromCpp(objcpp_result_);
} DJINNI_TRANSLATE_EXCEPTIONS()
}
- (void)setLoginProvider:(nullable id)provider {
try {
_cppRefHandle.get()->setLoginProvider(::djinni_generated::LoginProvider::toCpp(provider));
} DJINNI_TRANSLATE_EXCEPTIONS()
}
+o +j
而LoginProvider则实现了::wla_gen::LoginProvider
和::djinni::ObjcProxyCache::Handle
,一看就知道要甩锅给一个Objective-C对象。
namespace djinni_generated {
class LoginProvider
{
public:
using CppType = std::shared_ptr<::wla_gen::LoginProvider>;
using CppOptType = std::shared_ptr<::wla_gen::LoginProvider>;
using ObjcType = id;
using Boxed = LoginProvider;
static CppType toCpp(ObjcType objc);
static ObjcType fromCppOpt(const CppOptType& cpp);
static ObjcType fromCpp(const CppType& cpp) { return fromCppOpt(cpp); }
private:
class ObjcProxy;
};
} // namespace djinni_generated
//隐藏在WLALoginProvider+Private.mm文件中
class LoginProvider::ObjcProxy final
: public ::wla_gen::LoginProvider
, public ::djinni::ObjcProxyCache::Handle
{
public:
using Handle::Handle;
void setClientID(const std::string & c_client_id) override
{
@autoreleasepool {
[Handle::get() setClientID:(::djinni::String::fromCpp(c_client_id))];
}
}
LoginSDKImpl的逻辑主要是C++写的,主要是调LoginProvider做各种事情。这个地方可以写一些平台同样的逻辑,比如token的存取等。
void LoginSDKImp::setLoginProvider(const std::shared_ptr & provider)
{
_provider = provider;
}
void LoginSDKImp::setClientID(const std::string & client_id)
{
if (_provider)
_provider->setClientID(client_id);
}
Objective-C
LoginProvider要Objective-C来实现具体的逻辑,所以要有一个实现WLALoginProvider
协议的类。
@interface WLALoginProviderImpl : NSObject
@end
@interface WLALoginProviderImpl ()
@property(nonatomic, strong, nullable) id currentAuthorizationFlow;
@end
@implementation WLALoginProviderImpl
- (void)login:(nullable id)callback {
//使用AppAuth-iOS发起OAuth 2.0认证
}
djinni不足之处
djinni无法表达每个端专有的特性,具体到iOS端,有如下不足。
- 无法表达
nonull
、nullable
。对于普通属性统统使用nonull
,对于callback则使用nullable
。 - 无法表达协议的
@optional
。由于IDL往往是各端接口的超集,iOS只会实现其中某些接口,这样会导致很多警告。 - 跟所有IDL一样,无法使用各端特有的数据类型,无法使用泛型。
- 以上缺点都能忍,callback的问题实在是很难忍受,Objective-C和C++的Callback写起来都很麻烦。
Objective-C callback
所有+o +j
的callback都会变成协议,需要一个Objective-C包装类,使用起来非常之麻烦。
@interface WLALoginCallbackImpl : NSObject
- (instancetype)initWithBlock:(void(^)(WLAUser *, WLAError *))block;
@end
@interface WLALoginCallbackImpl ()
@property (nonatomic, copy) void(^block)(WLAUser *user, WLAError *error);
@end
@implementation WLALoginCallbackImpl
- (instancetype)initWithBlock:(void (^)(WLAUser *, WLAError *))block
{
self = [super init];
if (self) {
self.block = block;
}
return self;
}
- (void)call:(nonnull WLAError *)error
user:(nonnull WLAUser *)user {
if (self.block) {
self.block(user, error);
}
}
@end
//使用起来一点都没有block那种行云流水的感觉
[[WLALoginSDK sharedInstance] login: [[WLALoginCallbackImpl alloc]
initWithBlock:^(WLAUser *user, WLAError *error) {
}]];
C++ callback
C++层的block需要使用makeCallback构造出来,不算方便,但是比起Objective-C还是要好一些的。
void LoginSDKImp::login(const std::shared_ptr & callback)
{
if (_provider) {
std::weak_ptr weak_this = shared_from_this();
auto provider_callback = wla::makeCallback([weak_this, callback](const wla_gen::Error &err, const string &response) {
auto strong_this = weak_this.lock();
if (!strong_this)
return;
strong_this->handleLoginResponse(err, response, callback);
});
_provider->login(provider_callback);
}
}
make_callBack包装了一个lambda闭包。反正现在C++ 11的代码鬼都看不懂,贴一个lambda闭包的例子吧。
#include
#include
#include
#include
int main()
{
std::vector c = {1, 2, 3, 4, 5, 6, 7};
int x = 5;
c.erase(std::remove_if(c.begin(), c.end(), [x](int n) { return n < x; }), c.end());
std::cout << "c: ";
std::for_each(c.begin(), c.end(), [](int n){ std::cout << n << ' '; });
std::cout << '\n';
auto func1 = [](int n) { return n + 4; };
std::cout << "func1: " << func1(6) << '\n';
std::function func2 = [](int n) { return n + 4; };
std::cout << "func2: " << func2(6) << '\n';
}
$ clang -std=c++11 1.cpp -lstdc++ && ./a.out
c: 5 6 7
func1: 10
func2: 10
参考资料。
- djinni helloworld
- Dropbox经验谈:iOS和Android的C++跨平台开发
- Facebook应用Moments使用C++实现跨平台代码共享