djinni使用实践(二) -- djinni究竟都有哪些配置选项
Djinni
Djinni 是一个生成跨语言类型声明和接口绑定的工具。 它旨在将 C++ 与 Java 或 Objective-C 连接起来。 Python 支持在 python 分支上的实验版本中可用。
Djinni 可用于在 Android 和 iOS 上将跨平台 C++ 库代码与特定于平台的 Java 和 Objective-C 接口。 我们在 CppCon 2014 上发布了 Djinni。你可以看到幻灯片和视频。 有关 Djinni 以及其他人如何使用它的更多信息,请查看本文档末尾的社区链接。
维护说明:此 repo 是稳定的,但不再由 Dropbox 维护。 如果您有任何疑问或想与 Djinni 的其他用户交谈,您可以通过本文档末尾的链接加入 Slack 社区。
主要特点
- 从单个接口描述文件生成并行 C++、Java 和 Objective-C 类型定义。
- 支持三种核心语言的原始类型和用户定义的枚举、记录和接口的交集。
- 生成接口代码,允许在 C++ 和 Java(使用 JNI)或 Objective-C(使用 Objective-C++)之间进行双向调用。
- 可以自动生成数据类型的比较器函数(相等、排序)
入门
类型
Djinni 根据 IDL 文件中的接口定义生成代码。 IDL 文件可以包含三种声明:枚举、记录和接口。
- Enums成为 C++ enum类、Java enums或 ObjC NS_ENUM。
- Flags成为带有方便的面向位运算符的 C++ 枚举类、带有 EnumSet 的 Java 枚举或 ObjC NS_OPTIONS。
- Records是纯数据值对象。
- Interfaces是具有要调用的定义方法的对象(在 C++ 中,由 shared_ptr 传递)。 Djinni 生成的代码允许从 ObjC 或 Java 透明地使用在 C++ 中实现的接口,反之亦然。
IDL 文件
Djinni 的输入是一个接口描述文件。 下面是一个例子:
# Multi-line comments can be added here. This comment will be propagated
# to each generated definition.
#枚举
my_enum = enum {
option1;
option2;
option3;
}
#标记
my_flags = flags {
flag1;
flag2;
flag3;
no_flags = none;
all_flags = all;
}
#数据类
my_record = record {
id: i32;
info: string;
store: set<string>;
hash: map<string, i32>;
values: list<another_record>;
# Comments can also be put here
# Constants can be included
const string_const: string = "Constants can be put here";
const min_value: another_record = {
key1 = 0,
key2 = ""
};
}
another_record = record {
key1: i32;
key2: string;
} deriving (eq, ord)
# 定义一个接口,+c表示在c语言中实现这个接口
my_cpp_interface = interface +c {
method_returning_nothing(value: i32);
method_returning_some_type(key: string): another_record;
static get_version(): i32;
# 常量
const version: i32 = 1;
}
# 定义一个接口,+j +o分别表示在java/ObjectC(ios)语言中实现这个接口
my_client_interface = interface +j +o {
log_string(str: string): bool;
}
Djinni文件也可以互相包含。 添加行:
#路径为当前文件的相对路径
@import "relative/path/to/filename.djinni"
在文件开头,将仅包含另一个文件。 子文件路径是相对于包含@import的文件的位置而言的。 两个不同的djinni文件不能定义相同的类型。 @import的行为类似于在C ++中与#pragma一起#include的行为,或与ObjC的#import行为:如果通过不同的路径多次包含文件,则该文件只会被处理一次。
生成代码
当 Djinni 文件准备好后,您可以从命令行或 bash 脚本运行:
src/run \
--java-out JAVA_OUTPUT_FOLDER \ #生成的java文件的输出路径(相对路径),可自定义
--java-package com.example.jnigenpackage \ #生成的java文件的包名
--java-cpp-exception DbxException \ # 在 Java 中的自定义 C++ 异常和 java.lang.RuntimeException(默认)之间进行选择。
--ident-java-field mFooBar \ # 可选,这会在 Java 字段名称前添加一个“m”
\
--cpp-out CPP_OUTPUT_FOLDER \ #生成的C++文件的输出路径(相对路径)
\
--jni-out JNI_OUTPUT_FOLDER \ #生成的jni文件的输出路径(相对路径)
--ident-jni-class NativeFooBar \ # 在生成的JNI类(类名)加“Native”前缀(当然也可以是JNIFooBar,随意)
--ident-jni-file NativeFooBar \ # 这会为 JNI 文件名添加前缀,否则 cpp 和 jni 文件名相同
\
--objc-out OBJC_OUTPUT_FOLDER \ #生成的oc文件的输出路径(相对路径)
--objc-type-prefix DB \ # Apple 建议 Objective-C 类为每个定义的类型都有一个前缀
\
--objcpp-out OBJC_OUTPUT_FOLDER \
\
--idl MY_PROJECT.djinni #你定义的djinni文件
其他一些选项也可用,例如 --cpp-namespace 将生成的 C++ 代码放入指定的命名空间。 要查看所有选项的列表,请运行
src/run --help
示例生成代码位于此发行版的 example/generated-src/ 和 test-suite/generated-src/ 目录中。
请注意,如果未指定语言的输出文件夹,则不会生成该语言。 有关更多信息,请运行 run --help 以查看所有可用的命令行参数。
在项目中使用生成的代码
Java / JNI / C++ 项目
Includes & Build target
Type | C++ header | C++ source | Java | JNI header | JNI source |
---|---|---|---|---|---|
Enum/Flags | my_enum.hpp | MyEnum.java | NativeMyEnum.hpp | NativeMyEnum.cpp | |
Record | my_record[_base].hpp | my_record[_base].cpp (+) | MyRecord[Base].java | NativeMyRecord.hpp | NativeMyRecord.cpp |
Interface | my_interface.hpp | my_interface.cpp (+) | MyInterface.java | NativeMyInterface.hpp | NativeMyInterface.cpp |
(+) 仅为包含常量的类型生成。 将所有生成的源文件添加到您的构建目标,以及 support-lib/java 的内容。
JNI 方法
JNI 代表 Java 本地接口,是 Java 语言的扩展,允许与本地 (C/C++) 代码或库进行互操作。 有关 JNI 的完整文档可从以下网址获得:Contents
对于每种类型,内置(列表、字符串等)或用户定义,Djinni 生成一个带有 toJava 和 fromJava 函数的翻译器类来来回翻译。
应用程序代码负责 JNI 库的初始加载。 在代码中的某处添加一个静态块:
System.loadLibrary("YourLibraryName");
如果您将本地库打包在 jar 中,您还可以使用 com.dropbox.djinni.NativeLibLoader 来帮助解包和加载您的库。 有关详细信息,请参阅本地主机自述文件。
当调用本地库时,JNI 会调用一个名为 JNI_OnLoad 的特殊函数。 如果所有 JNI 接口代码都使用 Djinni,请包含 support_lib/jni/djinni_main.cpp; 如果没有,您需要添加对您自己的 JNI_OnLoad 和 JNI_OnUnload 函数的调用。 有关详细信息,请参阅 support-lib/jni/djinni_main.cpp。
Objective-C / C++ 工程
Objective-C/C++生成的文件如下(假设前缀为DB):
Type | C++ header | C++ source | Objective-C files | Objective-C++ files |
---|---|---|---|---|
Enum/Flags | my_enum.hpp | DBMyEnum.h | ||
Record | my_record[_base].hpp | my_record[_base].cpp (+) | DBMyRecord[Base].h | DBMyRecord[Base]+Private.h |
DBMyRecord[Base].mm (++) | DBMyRecord[Base]+Private.mm | |||
Interface | my_interface.hpp | my_interface.cpp (+) | DBMyInterface.h | DBMyInterface+Private.h |
DBMyInterface+Private.mm |
(+) 仅为包含常量的类型生成。 (++) 仅为具有派生操作和/或常量的类型生成。 这些具有 .mm 扩展名以允许非平凡常量
将所有生成的文件添加到您的构建目标,以及 support-lib/objc 的内容。 请注意,+Private 文件只能与 ObjC++ 源(其他标头是纯 ObjC)一起使用,并且接口的 Objective-C 用户不需要。
生成类型的详细信息
Enum
枚举被转换为底层类型为 int 的 C++ 枚举类、底层类型为 NSInteger 的 ObjC NS_ENUM 和 Java 枚举。
Flags
为方便起见,Flags被转换为 C++ 枚举类,其底层类型为无符号和生成的一组重载按位运算符,底层类型为 NSUInteger 的 ObjC NS_OPTIONS 和 Java EnumSet<>。 与上述枚举相反,标志的枚举代表单个位而不是整数值。
在 IDL 文件中指定标志类型时,您可以为选项分配特殊语义:
my_flags = flags {
flag1;
flag2;
flag3;
no_flags = none;
all_flags = all;
}
在上面的例子中,标有 none 和 all 的元素被赋予了特殊的含义。 在 C++ 和 ObjC 中,no_flags 选项是使用没有设置位(即 0)的值生成的,而 all_flags 是作为所有其他值的按位或组合生成的。 在 Java 中,不会生成这些特殊选项,因为可以只使用 EnumSet.noneOf() 和 EnumSet.allOf()。
Record
Record是数据对象。 在 C++ 中,Record按值包含其所有元素,包括其他记录(因此记录不能包含自身)。
数据类型
数据类、参数或返回值的可用数据类型有:
- Boolean (
bool
) - Primitives (
i8
,i16
,i32
,i64
,f32
,f64
). - Binary (
binary
). 这在 C++ 中实现为 std::vector<uint8_t>,在 Java 中实现为 byte[],在 Objective-C 中实现为 NSData。 - Date (
date
). 这是 C++ 中的 chrono::system_clock::time_point、Java 中的 Date 和 Objective-C 中的 NSDate。 - List (
list<type>
). 这是 C++ 中的 vector<T>、Java 中的 ArrayList 和 Objective-C 中的 NSArray。 列表中的原语将在 Java 和 Objective-C 中装箱。 - Set (
set<type>
). 这是 C++ 中的 unordered_set<T>、Java 中的 HashSet 和 Objective-C 中的 NSSet。 集合中的原语将被装箱在 Java 和 Objective-C 中。 - Map (
map<typeA, typeB>
). 这是 C++ 中的 unordered_map<K, V>、Java 中的 HashMap 和 Objective-C 中的 NSDictionary。 地图中的原语将在 Java 和 Objective-C 中装箱。 - Enumerations / Flags
- 可选(可选<typeA>)。 这是 C++11 中的 std::experimental::optional<T>,Java 中的对象/装箱原始引用(可以为 null),以及 Objective-C 中的对象/NSNumber 强引用(可以为 nil)。
- 其他数据类型。 这是使用按值语义生成的,即复制方法将对内容进行深度复制。
扩展
为了支持额外的字段和/或方法,可以在任何语言中用“extended”扩展数据类。 要以一种语言扩展数据类,您可以在record标记后添加 +c (C++)、+j (Java) 或 +o (ObjC) 标志。 生成的类型将有一个 Base 后缀,您应该创建一个没有扩展数据类型后缀的派生类型。
派生类型必须以与 Base 类型相同的方式构造。 接口将始终使用派生类型。
派生方法
对于record类型,支持 Haskell 风格的“派生”声明来生成一些常用方法。 Djinni 能够生成相等和顺序比较器,在 C++ 中实现为运算符重载,在 Java/Objective-C 中实现为标准比较函数。
注意事项:
- record中的所有字段都按照它们在记录声明中出现的顺序进行比较。 如果以后需要添加字段,请确保顺序正确。
- 集合类型、选项和布尔值不支持排序比较。
- 要比较包含其他record的record,内部record必须至少派生出与外部record相同类型的比较器。
Interface
仅适用于 C++ 的特殊方法
+c 接口(仅可在 C++ 中实现)可以使用特殊关键字 const 和 static 标记的方法在 C++ 中具有特殊效果:
special_methods = interface +c {
const accessor_method();
static factory_method();
}
- const 方法将在 C++ 中声明为 const,尽管这不能在其他语言中的调用者上强制执行,因为其他语言缺乏此功能。
- 静态方法将成为C++类的静态方法,可以在没有对象的情况下从其他语言调用。 这对于用作跨语言构造函数的工厂方法通常很有用。
异常处理
当在 C++ 中实现的接口抛出 std::exception 时,它将被转换为 Java 中的 java.lang.RuntimeException 或 Objective-C 中的 NSException。 what() 消息也将被翻译。
常数
常量可以在接口和记录中定义。 在 Java 和 C++ 中,它们是生成类的一部分; 在 Objective-C 中,常量名称是全局变量,带有接口/记录名称的前缀。 例子:
record_with_const = record +c +j +o {
const const_value: i32 = 8;
}
在 C++ 中将是 RecordWithConst::CONST_VALUE,在 Java 中将是 RecordWithConst.CONST_VALUE,在 Objective-C 中将是 RecordWithConstConstValue。
模块化和库支持
当为您的项目生成接口并希望将其提供给所有 C++/Objective-C/Java 中的其他用户时,您可以告诉 Djinni 生成一个特殊的 YAML 文件作为代码生成过程的一部分。 该文件随后包含 Djinni 在不同项目中包含您的类型所需的所有信息。 指示 Djinni 创建这些 YAML 文件由以下参数控制:
--yaml-out
: YAML 文件的输出文件夹(如果未指定,则禁用生成器)。--yaml-out-file :
如果指定所有类型将合并到一个 YAML 文件中,而不是为每种类型生成一个文件(相对于 --yaml-out)。--yaml-prefix :
添加到存储在 YAML 文件中的类型名称的前缀(default:""
)。
这样的 YAML 文件如下所示:
---
name: mylib_record1
typedef: 'record +c deriving(eq, ord)'
params: []
prefix: 'mylib'
cpp:
typename: '::mylib::Record1'
header: '"MyLib/Record1.hpp"'
byValue: false
objc:
typename: 'MLBRecord1'
header: '"MLB/MLBRecord1.h"'
boxed: 'MLBRecord1'
pointer: true
hash: '%s.hash'
objcpp:
translator: '::mylib::djinni::objc::Record1'
header: '"mylib/djinni/objc/Record1.hpp"'
java:
typename: 'com.example.mylib.Record1'
boxed: 'com.example.mylib.Record1'
reference: true
generic: true
hash: '%s.hashCode()'
jni:
translator: '::mylib::djinni::jni::Record1'
header: '"Duration-jni.hpp"'
typename: jobject
typeSignature: 'Lcom/example/mylib/Record1;'
---
name: mylib_interface1
typedef: 'interface +j +o'
(...)
---
name: mylib_enum1
typedef: 'enum'
(...)
YAML 文件中的每个文档都描述了一种 extern 类型。 example/example.yaml 中提供了所有字段的完整文档。 您还可以检查文件 test-suite/djinni/date.yaml 和 test-suite/djinni/duration.yaml 以了解您可以使用它做什么的一些实际工作示例。
要在项目中使用库类型,只需将其包含在 IDL 文件中并使用其名称标识符引用它:
@extern "mylib.yaml"
client_interface = interface +c {
foo(): mylib_record1;
}
只要您遵循所需的格式,就可以手动创建这些文件。 这允许您支持不是由 Djinni 生成的类型。 有关高级示例,请参阅 test-suite/djinni/duration.yaml 以及 test-suite/handwritten-src/cpp/Duration-objc.hpp 和 test-suite/handwritten-src/cpp/Duration-jni.hpp 中的随附翻译器. 手写翻译器实现以下概念:
// For C++ <-> Objective-C
struct Record1
{
using CppType = ::mylib::Record1;
using ObjcType = MLBRecord1*;
static CppType toCpp(ObjcType o) { return /* your magic here */; }
static ObjcType fromCpp(CppType c) { return /* your magic here */; }
// Option 1: use this if no boxing is required
using Boxed = Record1;
// Option 2: or this if you do need dedicated boxing behavior
struct Boxed
{
using ObjcType = MLBRecord1Special*;
static CppType toCpp(ObjcType o) { return /* your magic here */; }
static ObjcType fromCpp(CppType c) { return /* your magic here */; }
}
};
// For C++ <-> JNI
#include "djinni_support.hpp"
struct Record1
{
using CppType = ::mylib::Record1;
using JniType = jobject;
static CppType toCpp(JniType j) { return /* your magic here */; }
// The return type *must* be LocalRef<T> if T is not a primitive!
static ::djinni::LocalRef<jobject> JniType fromCpp(CppType c) { return /* your magic here */; }
using Boxed = Record1;
};
对于接口类,CppType 别名应为 std::shared_ptr<T>。
确保将翻译器放入具有代表性和不同的命名空间。
如果您的类型是通用的,则翻译器采用相同数量的模板参数。 在使用时,每个都使用相应类型参数的翻译器进行实例化。
template<class A, class B>
struct Record1
{
using CppType = ::mylib::Record1<typename A::CppType, typename B::CppType>;
using ObjcType = MLBRecord1*;
static CppType toCpp(ObjcType o)
{
// Use A::toCpp() and B::toCpp() if necessary
return /* your magic here */;
}
static ObjcType fromCpp(CppType c)
{
// Use A::fromCpp() and B::fromCpp() if necessary
return /* your magic here */;
}
using Boxed = Record1;
};
其他
构造函数/初始值设定项
Djinni 不允许为record或interface自定义构造函数,因为除非手动编辑自动生成的文件,否则无法在 Java 中实现它们。 相反,使用扩展记录或静态函数。
识别格式
Djinni 支持大多数生成的文件名和标识符的可覆盖格式。 可以通过使用 --help 调用 Djinni 来找到完整列表。 通过以所需的样式格式化单词 FooBar 来指定格式:
FOO_BAR
->GENERATED_IDENT
mFooBar
->mGeneratedIdent
FooBar
->GeneratedIdent
整数类型
在 Djinni 中,i8 到 i64 都是固定长度使用的。 不使用 C++ 内置 int、long 等和 Objective-C NSInteger,因为它们的长度因架构而异。 不包括无符号整数,因为它们在 Java 中不可用。
测试套件
运行 make test 以调用 test-suite 子目录中的测试套件。 它将在本地 JVMy 上构建和运行 Java 代码,并在 iOS 模拟器上构建和运行 Objective-C。 后者只能在带有 Xcode 的 Mac 上运行。
生成一个独立的 jar
根目录Makefile 的 djinni_jar 目标创建了一个独立的 .jar。 这在引擎盖下使用了 sbt 程序集插件。
只需从根目录调用:
make djinni_jar
这将在 src/target/scala_<SCALA_VERSION>/djinni-assembly-<VERSION>.jar 中生成一个 .jar 文件。
注意:生成jar需要sbt的环境,make djinni_jar前请确定正确安装sbt环境,如mac os运行以下命令安装:
brew install sbt
您可以将其作为任何其他可执行文件 .jar 移动和使用。
假设 .jar 位于 $DJINNI_JAR_DIR 其版本等于 0.1-SNAPSHOT:
# Example
java -jar $DJINNI_JAR_DIR/djinni-assembly-0.1-SNAPSHOT.jar \
--java-out "$temp_out/java" \
--java-package $java_package \
--java-class-access-modifier "package" \
--java-nullable-annotation "javax.annotation.CheckForNull" \
--java-nonnull-annotation "javax.annotation.Nonnull" \
--ident-java-field mFooBar \
\
--cpp-out "$temp_out/cpp" \
--cpp-namespace textsort \
--ident-cpp-enum-type foo_bar \
\
--jni-out "$temp_out/jni" \
--ident-jni-class NativeFooBar \
--ident-jni-file NativeFooBar \
\
--objc-out "$temp_out/objc" \
--objcpp-out "$temp_out/objc" \
--objc-type-prefix TXS \
--objc-swift-bridging-header "TextSort-Bridging-Header" \
\
--idl "$in"
注意:根 Makefile 的 all 目标包括 djinni_jar 目标。
生成支持库的 iOS 通用二进制文件
ios-build-support-lib.sh 可帮助您为 iOS 平台构建通用静态库。 它使用 ios-cmake 存储库的平台文件。
它基本上为每个 IOS_PLATFORM 变量创建一个通用静态库,并使用 lipo 将所有文件合并为一个。
基本上有两个变量要修改:
- BUILD_APPLE_ARCHITECTURES:指定要构建的 IOS_PLATFORM。 有关更多信息,请查看 GitHub - leetal/ios-cmake: A CMake toolchain file for iOS, macOS, watchOS & tvOS C/C++/Obj-C++ development。
- ENABLE_BITCODE:启用/禁用位码生成。
Android Parcelable 序列化
Djinni 支持生成实现 android.os.parcelable 的类。 为此,需要两个步骤:
- 使用关键字parcelable导出应该可分块的record: deriving(parcelable)
- 运行时加入此命令
--java-implement-android-os-parcelable true
社区链接
- Join the discussion with other developers at the Mobile C++ Slack Community
- There are a set of tutorials for building a cross-platform app using Djinni.
- mx3 is an example project demonstrating use of Djinni and other tools.
- Slides and video from the CppCon 2014 talk where we introduced Djinni.
- Slides and video from the CppCon 2015 talk about Djinni implementation techniques, and the addition of Python.
- You can see a CppCon 2014 talk by app developers at Dropbox about their cross-platform experiences.
作者
- Kannan Goundan
- Tony Grue
- Derek He
- Steven Kabbes
- Jacob Potter
- Iulia Tamas
- Andrew Twyman
联系我们
- Andrew Twyman -
artwymana+djinni@gmail.com
- Jacob Potter -
djinni@j4cbo.com