CppCon 2015 学习:Bridging Languages Cross-Platform
Djinni 是一个用于 跨平台 C++ 与移动平台(如 Android 和 iOS)之间接口绑定 的工具。它允许你定义一个跨平台的接口(通常是用一种类似 IDL 的语法),然后自动生成:
- C++ 实现的接口定义(共用的核心逻辑)
- Objective-C++ 接口桥接代码(iOS)
- Java JNI 桥接代码(Android)
用 Djinni 的场景:
假设你在开发一个移动应用(Android & iOS),并且你想将核心逻辑(比如业务模型、算法、数据处理)写在 跨平台的 C++ 层,而 UI 层则使用平台原生的技术(iOS 用 Swift/Obj-C,Android 用 Java/Kotlin)。
你可以用 Djinni 来:
- 定义一个跨平台接口,比如:
record SomeInfo {// 你定义的数据结构,比如 string name, int age } interface MyModel {doStuff(info: SomeInfo); }
- 然后 Djinni 会为你自动生成:
- C++ 头文件 & 实现 stub:你在这写跨平台核心逻辑
- Java 类和 JNI 桥接代码:Android 上可以调用这个接口
- Objective-C 接口桥接:iOS 上也能调用这个接口
回到你的例子:
struct SomeInfo { /* … */ };
class MyModel { void do_stuff(const SomeInfo & info);
};
如果你是用 Djinni 的话:
- 这个接口就会被定义在
.djinni
文件中 - 然后生成 Java、Obj-C 接口桥接代码
- UI 层就可以直接调用
MyModel
,无论是在 Android 还是 iOS 上
为什么用 Djinni?
- 复用逻辑:避免重复写 Android 和 iOS 的核心逻辑
- 统一接口设计:保持两端一致
- 自动生成桥接代码:省去 JNI 和 Objective-C++ 的烦恼
如果你“跟着我们的架构”在做(即跨平台核心逻辑 + 原生 UI),那么 Djinni 的作用就是:
你写一次接口
两端都能用
自动处理平台交互(JNI/Obj-C++)
Djinni 的核心工作流:
用 Djinni,你要做的四件事是:
1. Djinni IDL 文件(你定义接口的地方)
这是你写的 .djinni
文件,例如:
record SomeInfo {string name;int age;
}
interface MyModel {doStuff(info: SomeInfo);
}
这一步是 定义你想要的跨平台接口与数据结构,Djinni 会根据它生成桥接代码。
2. C++ 实现:MyModelImpl
类
你要手动实现一个继承自 Djinni 生成的 C++ 接口类,例如:
class MyModelImpl : public MyModel {
public:void doStuff(const SomeInfo & info) override {// 实际业务逻辑在这实现}
};
这是你写的核心逻辑,可以跨平台共享。
3. Java 调用代码(UI 层调用)
Android UI 层写 Java/Kotlin 代码去调用 Djinni 生成的 Java 桥接类:
SomeInfo info = new SomeInfo("Alice", 25);
MyModel model = MyModelImpl.create(); // Djinni 自动生成的工厂方法
model.doStuff(info);
4. Objective-C / Swift 调用代码(UI 层调用)
iOS UI 层使用 Swift 或 Obj-C 调用 Djinni 桥接类:
SomeInfo *info = [[SomeInfo alloc] initWithName:@"Alice" age:25];
id<MyModel> model = [MyModelImpl create];
[model doStuff:info];
总结一下:
文件类型 | 内容 | 谁写的 |
---|---|---|
1. .djinni | 定义接口 + 数据结构 | 你写的 |
2. C++ 实现 | MyModelImpl 业务逻辑实现 | 你写的 |
3. Java 调用 | UI 调用代码(Android) | 你写的 |
4. Obj-C 调用 | UI 调用代码(iOS) | 你写的 |
桥接代码 | JNI, Obj-C++, 工厂方法等 | Djinni 自动生成 |
Djinni 的价值:
你写一次 .djinni
接口定义,其余语言间的“翻译器”(桥接层)Djinni 帮你全自动生成,大幅减少了手写 JNI / Obj-C++ 的复杂工作,让你专注在:
- 写逻辑(C++)
- 写界面调用代码(Java / Swift)
Djinni 的作用,就是为你自动生成跨平台桥接层代码,以便你只需专注于接口定义与核心实现,而不用手写繁琐的 JNI 和 Objective-C++ 桥接逻辑。
Djinni 为你做的事情总结如下:
1. 接口类:MyModel 抽象类
Djinni 根据你的 .djinni
文件,生成以下语言的接口定义:
- C++:
class MyModel
(纯虚类,用于你继承实现) - Java:
interface MyModel
(用于 Android 侧调用) - Obj-C:
@protocol MyModel
(用于 iOS 侧调用)
这些接口统一定义了你的跨平台行为,保持各语言一致性。
2. 数据类:SomeInfo
Djinni 自动为你生成:
struct SomeInfo
in C++class SomeInfo
in Java@interface SomeInfo
in Objective-C
这些类都是值类型(record),在三种语言中都能一致使用。
3. JNI 桥接代码(Java ↔ C++)
Djinni 会生成多个 JNI 文件,用于:
- 在 Java 调用 C++ 的接口实现时:
- 映射 Java 调用到 C++
- 序列化/反序列化参数 & 返回值(如
SomeInfo
) - 保证跨线程、跨语言调用安全
你 不用写 JNI 代码,Djinni 自动生成!
4. Objective-C++ 桥接代码(Obj-C ↔ C++)
Djinni 会生成 Objective-C++ 文件,用于:
- 在 iOS 使用
MyModel
接口时调用 C++ 实现 - 将
SomeInfo
从 Obj-C 类型转为 C++ 类型,反之亦然 - 管理内存、所有权(ARC ↔ C++)
同样,你也不用手写 Objective-C++,Djinni 全包办。
总结:Djinni 自动生成的东西
分类 | 文件 | 你需要手写? | Djinni 生成? |
---|---|---|---|
接口类 | MyModel(C++/Java/ObjC) | ||
数据结构 | SomeInfo(C++/Java/ObjC) | ||
JNI 桥接 | Java ↔ C++ | ||
Obj-C++ 桥接 | ObjC ↔ C++ | ||
核心逻辑实现(MyModelImpl) | C++ 实现代码 | ||
UI 调用代码 | Java / ObjC 调用 | ||
IDL 文件 | .djinni 定义接口 | ||
总结一句话:Djinni 为你生成一整套跨平台 glue code,你只需专注于业务逻辑和 UI 调用。 |
这其实概括了 Djinni 的核心设计原则。我们来一条一条清晰解释:
Djinni 的设计原则详解
1. Developers interact with natural-looking code in all languages
开发者在每种语言中都用“原生风格”的代码。
- Java 开发者看到的是像普通 Java 类和接口
- Swift / Obj-C 开发者看到的是标准的 iOS 风格接口
- C++ 开发者则使用标准的类/结构体
你不用关心桥接细节,代码风格自然,易维护。
2. Method calls pass control between languages
方法调用时,控制权直接在线程中穿越语言边界。
- 不是异步调用,不需要序列化或网络传输
- 本质是 C++ 函数调用通过 JNI 或 Objective-C++ 被原生语言调用(同步)
不像 gRPC 或 Thrift 这类 RPC 框架,不涉及远程调用或多线程调度。
3. Not serialized RPC
Djinni 不是 RPC 系统,没有:
- 网络传输
- 序列化为字节流
- 客户端 / 服务端 架构
它是一种 语言间本地绑定机制,更轻量,适用于移动平台。
4. Callable interfaces can be referenced across languages
你定义的接口(interface)可以在任意语言中传递并调用。
- 比如:Java 创建的接口实例 → 传给 C++ → C++ 调用回 Java 方法
- 支持 回调机制,语言间互相引用函数对象
非常适合实现监听器、回调、事件等结构。
5. Data is copied across languages
数据结构(record 类型)在语言间是 值拷贝传递(copy-by-value)。
SomeInfo
等结构体在 Java ↔ C++、ObjC ↔ C++ 之间会复制字段值- 没有共享内存或指针引用
- 避免内存泄漏或生命周期混乱问题
更安全、跨线程无忧,但大型数据结构可能会有性能影响(可优化)。
Djinni 不是:
- RPC 框架(比如 gRPC、Thrift)
- 数据持久化或网络传输工具
- 通用序列化框架
Djinni 是:
- 一个 多语言绑定生成工具
- 用于移动端(Android/iOS)共享 C++ 逻辑
- 提供 轻量、自然、跨语言调用机制
Djinni 的这部分特性。我们来逐条解释清楚,确保你对 Djinni 的 records 和 enums 概念及其行为有全面准确的理解:
Djinni 的核心类型系统理念
1. “Own and call from any language”
接口(interface)或实例是可以被任何语言持有和调用的:
- 你可以在 Java 中创建一个实现,传到 C++
- 或者 C++ 创建一个对象,传到 Swift
- 接口支持跨语言“回调”和“调用”
非常适合设计回调(callback)、监听器(listener)模式
2. Records = like structs
Djinni 中的 record
就像结构体(struct):
record SomeInfo {string name;int age;
}
生成后:
- C++:是个
struct SomeInfo
- Java:是个不可变的 POJO 类
- ObjC:是个 Objective-C 对象(有属性)
3. Immutable data, implemented for you, marshaled by copy
Record 类型是 不可变数据结构,所有语言中:
- 只有只读属性(no setters)
- 构造时传值,之后只读
- 在语言之间传递时是 值拷贝
没有引用、指针、共享内存
安全
但大型结构可能影响性能(建议避免大嵌套)
4. Contain data (not records) by value (no recursion)
Record 中的字段类型必须是:
- 基础类型(如
int
,string
,bool
) - 其他 Djinni 支持的非 record 类型(如
optional
,list
,map
)
不允许嵌套 record:
# 合法
record A {string name;
}
# 不合法(嵌套 record)
record B {A a; // 错误
}
避免复杂嵌套,有助于简单高效的序列化和桥接
5. Enums = like scoped enums
Djinni 的 enum
就像 C++11 的 enum class
,有作用域:
enum Status {ok;error;
}
- 各语言中都会生成同名 enum 类型
- 所有语言定义的是 相同值集
- 可安全用于条件分支、switch 等
总结对比表:
特性 | Djinni 行为 |
---|---|
Interface 拥有/调用 | 所有语言中都能持有、传递、调用 |
Record 类型 | 类似 struct,不可变,字段拷贝 |
Record 限制 | 不能嵌套 record;字段是简单值或集合 |
Enum 类型 | 像 C++ scoped enum,所有语言值一致 |
数据传输方式 | 跨语言传递时拷贝(by value) |
内存管理 | 各语言自动管理对象生命周期 |
如果你想,我可以帮你写一个完整的例子,包括: |
enum Status
record SomeInfo
interface MyModel
带参数调用- 各语言中的实际使用代码
你列举的这些都是 Djinni 支持的附加功能(“other stuff”),用于让你的跨语言接口更完整、更实用。我们来快速解释每一项:
Djinni 支持的“其他东西”详解:
1. Primitive types
Djinni 支持的基本类型包括:
bool
→ 布尔值i8
,i16
,i32
,i64
→ 有符号整数f32
,f64
→ 浮点数(float/double)string
→ 字符串(跨语言自动映射)binary
→ 字节数组(用于传文件、图片、加密数据等)
这些类型可以用于 records、方法参数、返回值
2. Containers, Optionals
Djinni 支持标准集合和可选值类型:
list<T>
→ 映射为:- Java:
List<T>
- C++:
std::vector<T>
- Obj-C:
NSArray<T>
- Java:
set<T>
、map<K, V>
同理optional<T>
→ 对应各语言的可选类型(Java 中是 nullable)
很适合表示:有/无数据、多项结果、键值对等
3. Static methods
Djinni 允许定义静态方法:
interface Utils {static computeHash(data: binary): string;
}
生成后:
- Java/ObjC 会生成类方法
- C++ 中是静态函数
不需要创建对象也能调用的工具方法
4. Constants
你可以定义常量(enum-like 或静态值):
const string appName = "MyApp";
const i32 maxItems = 100;
这些常量会被嵌入到对应语言的类或命名空间中,保持一致。
避免魔法数字 / 重复定义
5. Extensible and external types
Extensible enums
Djinni 支持你在 enum 定义中为未来扩展预留空间:
enum Status {ok;error;unknown; // fallback
}
可以处理未知值(适用于版本兼容性)
External types
你可以声明外部类型,让 Djinni 把某些类型映射为已有类:
type dateTime = "std::chrono::system_clock::time_point"+ "NSDate*"+ "java.time.Instant";
这样你就可以在接口中直接使用这些平台已有类型,而不用重新定义。
有效利用平台特性
保持代码简洁
6. … and more on GitHub
Djinni 还有更多细节支持,比如:
- 异常传递(通过 Result 类型)
- 枚举值 fallback 策略
- 自定义类型映射模板(模板化代码生成)
- 自定义命名风格、目录结构等配置项
你可以在其 GitHub 项目中查看更多说明和示例:
https://github.com/dropbox/djinni
总结一句话:
Djinni 不只是桥接接口,它是一个 设计完善的跨语言绑定系统,提供你构建安全、自然、高效的跨平台调用模型所需的一切。
.djinni
文件定义是完全正确的,并且非常典型,展示了 Djinni 的多种能力。我们来详细逐句解释,确保你完全理解每一部分的含义和 Djinni 所生成的内容。
你的 Djinni 定义结构分析:
### 1. wish_difficulty = enum { ... }
wish_difficulty = enum {easy;medium;hard;
}
- 定义了一个有作用域的枚举类型(像 C++ 的
enum class
) - 在 Java、C++、Obj-C 中都会生成对应的
wish_difficulty
枚举类型 - 所有语言的枚举值保持一致
用于描述 wish 的难度等级
2. wish = record { ... } deriving (eq, ord)
wish = record {difficulty: wish_difficulty;request: string;
} deriving (eq, ord)
- 定义了一个不可变数据结构(结构体)
wish
- 包含两个字段:
difficulty
(你前面定义的枚举)request
(字符串描述请求)
deriving (eq, ord)
表示:
- Djinni 会在所有语言中为
wish
自动生成:- 相等比较(==)
- 排序比较(<、>)
可用于放入集合、排序等操作
在 C++ 生成operator==
,operator<
等
3. djinni = interface +j +o { ... }
djinni = interface +j +o {const max_wishes: i32 = 3;grant_wish(my_wish: wish): bool;past_wishes(): set<wish>;static rub_lamp(): djinni;
}
关键点解释:
interface
:定义了一个跨语言可调用的接口+j
:生成 Java 接口+o
:生成 Objective-C 接口
这表示:你希望这个服务由 C++ 实现,供 Java & ObjC 调用
接口内容解读:
const max_wishes: i32 = 3;
- 跨语言常量
- 会在 Java/C++/ObjC 中生成等值静态常量
- 可直接使用,无需调用
grant_wish(my_wish: wish): bool;
- 接收一个
wish
对象,返回布尔值 - 会生成跨语言桥接函数,让 Java/ObjC 调用 C++ 实现
- 接收一个
past_wishes(): set<wish>;
- 返回一组之前的愿望
- 使用
set
集合,Djinni 会在所有语言中生成对应类型(如 Java 的Set
, C++ 的std::set
)
static rub_lamp(): djinni;
- 工厂静态方法,创建接口实例
- 你在 C++ 中实现这个方法,返回一个
djinni
接口对象 - Java/ObjC 中可通过类调用:
Djinni.rubLamp()
等
Djinni 会为你生成什么?
类型 | C++ | Java | ObjC |
---|---|---|---|
wish_difficulty | enum class wish_difficulty | enum WishDifficulty | typedef NS_ENUM(...) |
wish | struct wish | final class Wish | @interface Wish |
djinni 接口 | class djinni (纯虚类) | interface Djinni | @protocol Djinni |
桥接代码 | JNI, Obj-C++ 实现桥接层 | 使用 Java 调用 C++ | 使用 ObjC 调用 C++ |
总结一句话:
你定义了一个可调用的“许愿平台服务”,包含枚举、数据结构、常量、集合、工厂方法,并支持从 Android(Java)和 iOS(ObjC)调用共享的 C++ 实现。
将 Djinni 拓展到支持新语言(Python) 的同步进度概要。它体现了在为 Djinni 添加 Python 支持时的工程设计考虑与技术实现。我们一条条来详细解释,以帮助你理解整体脉络。
理解 Djinni 拓展语言支持(以 Python 为例)
1. Djinni overview
这是指 Djinni 的基本工作方式和设计哲学:
- 使用
.djinni
文件定义接口、数据、常量等 - 自动生成语言之间的桥接代码(Java/ObjC ↔ C++)
- 倾向于自然、类型安全、跨平台一致的接口
- 已支持 Java(Android)、Obj-C(iOS),现在探索添加 Python
2. Adding a new language (Python)
当要支持 Python 时,需要:
- 让 Python 能像 Java/ObjC 一样调用 C++ 逻辑
- 生成 Python ↔ C++ 的绑定代码
- 保持 Djinni 的风格(自然、类型安全、自动化)
目标是让 Python 能无缝使用由 Djinni 定义的接口与结构。
3. Problems presented by bridging to a new language
为 Djinni 添加 Python 支持时面临的挑战包括:
数据表示差异:
- Python 是动态类型语言,C++ 是静态类型语言 → 类型匹配复杂
- Python 没有 struct 或 enum 的直接对应 → 需要包装类
- 内存管理方式完全不同(Python 有 GC,C++ 无)
函数调用机制差异:
- Python 使用解释器,C++ 是本地编译执行 → 调用方式不兼容
- 多线程模型不同,Python 有 GIL(Global Interpreter Lock)
缺乏现成桥接机制:
- Java 使用 JNI,Obj-C 使用 Obj-C++
- Python 缺少统一的官方 C++ 接口标准
- 必须自己设计桥接结构(可能依赖 pybind11、Boost.Python 等)
4. Solutions we used for C++ to Python
为了解决上述问题,团队通常采用以下方案:
使用 pybind11
- 现代、轻量、无依赖的 C++11 库
- 能把 C++ 类、函数、enum、结构体等直接暴露给 Python
- 自动处理类型转换、引用计数等问题
- 代码风格自然,容易集成进 Djinni 的代码生成流程中
自定义模板生成 Python 绑定代码
- 在 Djinni 的 codegen 模板中增加 Python 后端
- 为每个 interface/record/enum 自动生成 pybind11 包装代码
- 自动生成 Python
class
/enum
,包装 C++ 后端逻辑
明确生命周期管理策略
- 对象由 C++ 拥有,Python 仅引用(或反之)
- 使用
std::shared_ptr
与 pybind11 的智能绑定支持
使用 setuptools
, pyproject.toml
打包为 Python 模块
- 生成
.so
或.pyd
可被 Python 导入使用 - 封装为 pip 可安装包(甚至跨平台发布)
5. Implementation techniques
具体的技术实现细节可能包括:
- 为每个
.djinni
类型生成:- C++:业务逻辑代码
- Python:API 封装类
- pybind11:桥接 glue code
示例技术细节:
// pybind11 binding example
py::class_<MyModel>(m, "MyModel").def("grant_wish", &MyModel::grant_wish).def_static("rub_lamp", &MyModel::rub_lamp);
- 支持枚举类型转换:
py::enum_<wish_difficulty>(m, "WishDifficulty").value("Easy", wish_difficulty::easy).value("Medium", wish_difficulty::medium).value("Hard", wish_difficulty::hard).export_values();
- 将 Djinni 的代码生成后端扩展,支持 Python 语言模板
总结
项目 | 内容说明 |
---|---|
Djinni 概览 | 跨语言接口生成工具 |
添加新语言(Python) | 设计 Python↔C++ 桥接 |
遇到的问题 | 类型系统、内存管理、调用方式差异 |
解决方案 | 使用 pybind11、自定义模板、生命周期管理 |
技术实现方式 | 自动生成 pybind11 封装、Python 包 |
将新语言(如 Python)接入 Djinni 的基本策略。我们来详细讲解你列出的每一步,让你更清楚整个流程的工程意义和技术要点。
Basic Steps of Our Approach(将新语言接入 Djinni 的四个核心步骤)
1. Pick a representation for IDL types
为 Djinni IDL 中的类型选择目标语言的表示方式
目标:
为 Djinni 中的 record
, enum
, interface
, primitive
, container
等定义,在新语言中选择自然、兼容的数据类型。
例如:
Djinni 类型 | Python 表达 |
---|---|
string | str |
i32 | int |
list<T> | List[T] |
optional<T> | Optional[T] |
record wish | Python dataclass |
enum difficulty | Python Enum |
interface djinni | Python class (wrapper) |
保持风格一致、安全、易用 | |
注意类型对齐和边界情况(如空值、嵌套) |
2. Pick a bridging technology
选择一种桥接机制,完成语言之间的互通调用
常见方案:
- Python:
- pybind11 推荐
- Boost.Python(重但强大)
- Cython(更偏 Python → C)
- SWIG(老牌自动生成器)
pybind11 优点:
- 现代 C++ 风格、轻量、无依赖
- 与 Djinni 的目标一致:自然、安全、自动
- 支持 class、enum、shared_ptr、异常等功能
- 好集成进 Djinni 的代码生成流程
3. Does it meet the basic requirements?
验证桥接技术是否满足 Djinni 的核心设计要求:
要求包括:
要求 | 示例 |
---|---|
支持调用 C++ 函数 | Python 能调用 grant_wish 等接口方法 |
支持映射复杂数据类型 | record、list、optional 正确转为 Python |
可处理内存和生命周期 | 通过 shared_ptr 实现安全持有 |
不需要显式序列化 | 不是 gRPC/REST,而是原生内存结构 |
错误/异常可映射 | C++ 异常 → Python 异常(可选) |
如果都满足,就可以继续构建高级特性 | |
如果不满足,可能需要自己写一部分 glue code |
4. Build features on top of those basics
在基本桥接功能上,构建完整支持:
你可以继续扩展支持:
- 枚举 fallback 支持
- constants 映射为模块级常量
- 异常/错误处理映射
- Python 类型注解(PEP484)
- 打包为 wheel 模块(可 pip 安装)
- 运行时回调(从 C++ 调 Python)
- 绑定泛型容器(list 等)
最终目标:
让 Python 像 Java/ObjC 一样自然地调用 C++ 实现的接口逻辑,
通过 Djinni 定义共享接口,实现多语言平台下的一致开发体验。
为 Djinni 映射 Python 类型时的设计原则。我们来一条条解释,帮助你深入理解 Step 1: Python Types 的核心理念:
Step 1: Python Types — 深度理解
“Be idiomatic.”
生成的代码要符合 Python 的“语感”。
- 意思是:不要让 Python 看起来像 C++ 的镜像。
- 例如,不要出现
get_name()
,set_name()
,而是使用属性name
- 用
snake_case
而不是camelCase
,符合 PEP8 - 用 Pythonic 的
__str__
,__eq__
,不要用 verbose 的函数名
示例(对比):
# Pythonic
class Wish:def __init__(self, difficulty: WishDifficulty, request: str):self.difficulty = difficultyself.request = request
# 太像 C++
class wish:def getDifficulty(self): ...def setRequest(self, r): ...
“Generated classes should look like Python.”
结构体(records)、接口(interfaces)应该像普通 Python 类一样自然。
- 不要用奇怪的封装或代码生成框架样式
- 推荐使用
@dataclass
(Python 3.7+) 或普通 class(兼容 2.x)
示例:
from dataclasses import dataclass
from enum import Enum
class WishDifficulty(Enum):EASY = 0MEDIUM = 1HARD = 2
@dataclass
class Wish:difficulty: WishDifficultyrequest: str
“Use native Python types the programmer expects.”
用 Python 的内建类型来表示数据,而不是自定义或 C++ 兼容包装。
Djinni 类型 | Python 类型 |
---|---|
i32 | int |
string | str |
list<T> | list[T] |
set<T> | set[T] |
optional<T> | Optional[T] 或 None |
record | @dataclass 或 class |
示例:不要用 CppVector 、CppOptional 之类中间类型 |
“Support the expected flexibility (duck typing).”
Python 的核心哲学是鸭子类型(duck typing)——如果它像鸭子、会叫,就当它是鸭子。
- 生成的类型应该可以自然使用,用户无需知道底层是 C++ 对象
- 支持传入 dict/list 等,能自动转换成 Djinni 类型(比如 record)
- 用户不应受限于类型硬编码
示例:
model.grant_wish(Wish(difficulty=WishDifficulty.EASY, request="ice cream"))
# 或更动态:
model.grant_wish({"difficulty": "EASY", "request": "ice cream"})
“Support both Python 2, and Python 3.”
向后兼容旧版本(历史原因)
- 需要生成的代码同时在 Python 2.7 和 Python 3.x 下能运行
- 避免使用 Python 3 特有语法(如 f-string, type hint)或提供替代
- 可以通过
six
/future
包实现兼容
注意:现代项目大多只需支持 Python 3,但历史项目有需求
总结
原则 | 目的 |
---|---|
生成 Pythonic 的类 | 让开发者感觉自然,易读易用 |
使用原生类型 | 减少学习成本,提高兼容性 |
支持灵活的参数和结构传入 | 拥抱 Python 的动态语言特性 |
保持 Python 2 和 3 兼容性 | 兼顾老项目与新项目需求 |
#关于 Djinni 里类型映射到 Python 基础类型时的具体细节说明,下面帮你逐条详细解析: |
Python Types: Basics — 详细说明
1. Numbers: integer/long, float
- Python 的整数类型并不像 C++ 的
i32
/i64
那么严格限制大小:- Python 2 中
int
是 32-bit,long
是无限大小整数 - Python 3 只有
int
,自动支持任意大小整数(没有区分 long)
- Python 2 中
- 浮点数统一用
float
(对应 C++ 的 double)
因此 Djinni 中的整数对应 Python 的int
或long
,浮点数对应 Python 的float
,对大小限制没有严格要求。
2. Containers: list, dict, set
- Djinni 的容器类型对应 Python 中最自然的容器类型:
list<T>
→ Pythonlist
map<K,V>
→ Pythondict
set<T>
→ Pythonset
- 这些容器是 Python 标准内建类型,使用方便且语义清晰
3. Strings: unicode (Python 2.7) or str (Python 3.x)
- Python 2:
unicode
类型才是文本字符串,str
是字节序列 - Python 3:
str
是文本字符串,bytes
是字节序列
Djinni 生成代码要区分版本,确保字符串编码正确,避免乱码。
4. Bytes: str (Python 2.7) or bytes (Python 3.x)
- 对应二进制数据:
- Python 2 用
str
代表字节串 - Python 3 用
bytes
代表字节串
- Python 2 用
- 需要在绑定层做对应的类型转换和处理
5. Optional: determines if None is legal
- Djinni 的
optional<T>
在 Python 映射时,允许字段值为None
表示“无值” - 这符合 Python 习惯的空值语义
- 对应的非 optional 字段则不允许为
None
总结表格
Djinni 类型 | Python 类型 (Py2 / Py3) | 说明 |
---|---|---|
integer (i32, i64) | int / long / int | Python int 动态大小 |
float | float | 浮点数类型 |
list | list | 列表容器 |
map<K,V> | dict | 字典容器 |
set | set | 集合容器 |
string | unicode / str | 文本字符串 |
bytes | str / bytes | 二进制数据 |
optional | 允许 None | 可空类型 |
这样就确保 Python 端数据与 C++/Java/ObjC 端的数据类型能正确且自然地对应,方便调用与传递。 |
Djinni 中的 record 类型 映射到 Python 时的表示方式,重点在于 Python 类的构造和属性定义。
Python Types: Records — 详细解析
1. Python class with named fields
- Djinni 的
record
类型对应到 Python,就是一个类(class
),包含带名字的属性字段。 - Python 的类不需要预先声明字段,字段通常在
__init__
构造函数中定义。
2. Python only has inclusion by reference
- Python 中所有对象都是通过引用传递的,没“值传递”的语义(不像 C++ 的结构体复制)
- 所以 Python 的 record 类实例是引用类型,赋值或传参时是传引用,不是复制内容。
3. __init__
is all that defines the fields
- Python 类的属性字段一般通过构造函数参数来初始化和定义:
class SomeInfo:""" Info used by my data model. """def __init__(self, user_id, obj_id, color):self.user_id = user_idself.obj_id = obj_idself.color = color
- 这也是 Python idiomatic 的写法,没有像 C++ 里那样的成员声明和类型约束
- 字段类型可用注解(Python 3.5+),但不是必须
4. 如何在 Djinni 代码生成时体现
- Djinni 在生成 Python 代码时,会为每个 record 生成类似上面代码的 Python 类
- 支持自动生成
__eq__
,__repr__
等方法增强可用性(用@dataclass
就更简洁)
额外示例:
class Wish:def __init__(self, difficulty, request):self.difficulty = difficultyself.request = request
# 使用示例
w = Wish(difficulty="easy", request="Get ice cream")
print(w.request) # 输出: Get ice cream
小结:
重点 | 说明 |
---|---|
record 映射为 Python class | 属性在 __init__ 中定义 |
通过引用传递对象 | Python 对象是引用,非值复制 |
字段名灵活定义 | 动态添加属性,兼容鸭子类型 |
Djinni 中的 enum 类型 在 Python 里的映射,尤其是用 enum
模块(Python 3.4+ 引入),并且兼容 Python 2 的做法。
我帮你总结并详细说明:
Python Types: Enums — 详细解析
1. Enum type introduced in Python 3.4
- Python 3.4 起,标准库增加了
enum
模块,提供了类型安全且语义清晰的枚举支持。 - 枚举成员是命名常量,方便表达状态、类型、类别等。
2. Available back-ported to Python 2
- Python 2 没有内置
enum
,但可以通过安装enum34
包获得同样功能。 - Djinni 生成的 Python 代码通常会兼容两个版本,自动导入这个包(或者用条件导入)。
3. IntEnum also acts like an int, for compatibility
IntEnum
是Enum
的子类,枚举成员同时是int
类型。- 这样枚举成员既是枚举,也可以当做整数使用,方便与 C++ 中的
enum
对应。
4. 示例代码
from enum import IntEnum, unique
@unique
class Color(IntEnum):Red = 0Green = 1Blue = 2
# 使用示例:
c = Color.Red
print(c) # 输出: Color.Red
print(int(c)) # 输出: 0
print(c == 0) # True,因为是 IntEnum
5. 如何结合 Djinni
- Djinni
.idl
中定义的enum color { Red; Green; Blue; }
会生成类似以上的 Python 枚举类 - 保证各语言间的枚举值对应一致
@unique
装饰器确保没有重复值,保持定义安全
小结表格
特点 | 说明 |
---|---|
enum 模块(3.4+) | 标准库枚举支持 |
enum34 包(Python 2) | 后向兼容支持 |
IntEnum | 枚举成员兼具整数类型的行为 |
@unique 装饰器 | 防止枚举成员重复 |
Djinni 定义的 interface 在 Python 里的映射方式,尤其是用 Python 的抽象基类(ABC)来表示接口。
我帮你详细拆解这部分:
Python Types: Interfaces — 详细说明
1. Python class expected to have specific methods
- Djinni 里的
interface
对应 Python 里的“约定接口”,就是类必须实现某些方法。 - 这种接口并不是语法强制的,但用抽象基类(ABC)可以在运行时强制实现。
2. Enforceable abstract base class introduced in Python 3
- Python 3 标准库有
abc
模块,提供了ABC
和ABCMeta
用于定义抽象基类。 - 通过
@abstractmethod
装饰的方法必须被子类重写,否则实例化会报错。
3. Available back-ported to Python 2
- Python 2 同样可以用
abc
模块(内置),但没有ABC
基类。 - 需要用
six.with_metaclass(ABCMeta)
来实现同样功能。 - 这样保证 Python 2 和 3 代码都可以定义抽象基类。
4. 示例代码
from abc import ABCMeta, abstractmethod
from six import with_metaclass # 用于兼容 Python 2 和 3
class MyModel(with_metaclass(ABCMeta, object)):""" Represents the data model of my app. """@abstractmethoddef do_stuff(self, info):""" Does something with the given info. """raise NotImplementedError
with_metaclass(ABCMeta, object)
定义了一个抽象基类do_stuff
是必须实现的方法- 任何直接实例化
MyModel
会失败,必须先实现这个方法的子类
5. 如何和 Djinni 接口配合
- Djinni 生成 Python 接口类时,自动生成抽象基类代码
- 用户在 Python 端继承并实现该接口,确保契约一致
- 结合 pybind11 等桥接实现 C++ 到 Python 的调用互通
小结表格
特性 | 说明 |
---|---|
抽象基类(ABC) | 明确规定必须实现的方法 |
abc 模块支持 | Python 2/3 皆可用 |
@abstractmethod | 声明接口方法,子类必须重写 |
兼容性手段 | 使用 six.with_metaclass 实现跨版本 |
静态类型语言(C++)和动态语言(Python) 在类型约束上的差异,特别是 Python 的鸭子类型(duck typing)哲学。
我帮你总结并详细说明:
Duck Typing 与 Djinni 多语言桥接 — 详细说明
1. C++ 程序员喜欢严格类型检查
- C++ 是静态类型语言,编译期类型检查非常严格
- 对象类型、接口必须明确定义,类型安全是首要考虑
2. Python 程序员不那么在意
- Python 是动态类型语言,没有编译期类型检查
- 只要对象“行为像某类型”,就可以用(鸭子类型)
- 不强制继承接口,只要实现了必要方法即可用
3. Djinni 提供工具支持
- Djinni 生成的 Python 类型和接口,提供标准类和抽象基类
- 也提供机制让用户自定义代码可以“表现得像” Djinni 类型
- 例如,只要一个对象有需要的方法和属性,就可以当作该类型使用,无需强制继承
4. 重点:只要用户代码行为像 Djinni 类型,一切正常
- 在 Python 端,如果你的对象有正确的方法签名和属性
- 即使它没有继承 Djinni 生成的抽象基类或数据类
- 仍然可以被 Djinni 桥接调用代码接受和使用
5. 示例
假设 Djinni 生成了一个接口类 MyModel
,只要你的 Python 类有 do_stuff(info)
方法:
class MyDuckModel:def do_stuff(self, info):print("Doing stuff with", info)
model = MyDuckModel()
some_function_accepting_MyModel(model) # 只要调用方用到了 do_stuff,就没问题
总结
观点 | 说明 |
---|---|
C++ 类型严格 | 静态检查,编译期错误防范 |
Python 更灵活(鸭子类型) | 只要“看起来像”,就能用,不必强制继承接口 |
Djinni 支持两种风格 | 提供抽象基类等静态接口,也支持鸭子类型使用 |
用户代码只需“行为匹配” | 极大提高灵活性,方便快速开发和测试 |
Python 加入 Djinni 绑定时选择桥接技术的第二步,使用 CFFI(C Foreign Function Interface)作为桥接工具。
Step 2: Pick a Bridging Technology — 使用 CFFI
1. 什么是 CFFI?
- CFFI 是 Python 的一种调用 C 语言接口的工具,全称是 C Foreign Function Interface。
- 让 Python 代码可以方便且高效地调用 C/C++ 库的函数和类型。
2. 为什么选 CFFI?
- 广泛支持:支持 CPython(官方 Python 实现)和 PyPy(JIT 优化的 Python 实现)。
- 兼容版本:支持 Python 2 和 Python 3,方便维护跨版本代码。
- 性能优越:生成的绑定代码是编译后的,运行时性能好,接近原生调用。
- 多种工作模式:包括 API Mode(头文件模式)、ABI Mode(动态调用模式)等。
Djinni 使用的是 API Mode, Out-of-Line,即编译时生成绑定代码,运行时加载。
3. API Mode, Out-of-Line
- API Mode:基于 C 头文件声明,生成对应的 Python 绑定接口。
- Out-of-Line:绑定代码在独立的 C 文件中编译,不是在 Python 运行时动态解析头文件。
- 这种方式更稳定、快速,适合复杂项目。
4. 如何和 Djinni 结合
- Djinni 生成的 C++ 接口通过 CFFI 编译成 Python 可调用的模块。
- Python 端通过 CFFI 调用 C++ 层实现的接口,桥接两边代码。
5. 优势
- 跨语言调用顺畅
- 保持高性能
- 保证跨 Python 版本和实现的兼容性
- 方便维护和扩展
总结
特性 | 说明 |
---|---|
CFFI | Python 调用 C/C++ 的桥接工具 |
支持 CPython & PyPy | 主流 Python 实现兼容 |
支持 Python 2 和 3 | 跨版本兼容 |
API Mode, Out-of-Line | 编译时绑定,运行时高效调用 |
生成高性能绑定代码 | 接近原生性能 |
描述的是用 CFFI 在构建时(build time)生成 Python 绑定的工作流程,具体如下:
CFFI at Build Time — 工作流程解析
1. C++ 代码 + C 包装器
- 先将 C++ 实现的代码和对应的 C 语言包装函数(wrapper)编译成一个动态库,比如
libfoo.dylib
(Mac 下动态库后缀)。 - C 包装器的作用是暴露简化且 C ABI 兼容的接口,方便 CFFI 调用。
2. Djinni 生成 CFFI 声明
- Djinni 根据接口定义,生成对应的 C 函数声明给 CFFI,例如:
void cw__foo_setmsg(DjinniWrapperFoo * self, const char * msg);
- 这些声明告诉 CFFI 这些函数签名和调用约定。
3. CFFI 生成调用代码
- CFFI 根据声明自动生成对应的 C 代码,这部分代码是 Python 调用 C 库的桥梁。
- 这段代码封装了从 Python 调用到底层 C 函数的细节。
4. 编译成 Python 扩展模块
- 生成的 C 代码被编译成 Python 的扩展模块(如
foo_cffi.so
),Python 可以直接导入使用。 - 这个扩展模块负责把 Python 的调用转换成底层 C++ 库调用。
5. 结果
- 最终产物是针对特定平台和 Python 版本的高效编译二进制,方便 Python 直接调用 C++ 代码。
- 性能好、调用流畅,适合跨语言调用。
总结流程图
C++ 源码 + C wrapper↓ 编译libfoo.dylib(动态库)
Djinni IDL → C 函数声明 → CFFI
CFFI 生成调用代码↓ 编译
foo_cffi.so(Python 扩展模块)
Python 代码 ←调用→ foo_cffi.so ←调用→ libfoo.dylib
CFFI 在运行时(run time)的调用流程,具体是:
CFFI at Run Time — 运行时调用流程
1. 加载编译好的扩展模块
- 在 Python 代码中用普通模块导入方式加载编译好的 CFFI 扩展模块:
from foo_cffi import lib
lib
对象是 CFFI 自动生成的接口,里面包含了所有 C 函数的绑定。
2. 直接调用 C 函数
- 通过
lib
调用 C 包装器暴露的函数,比如:
lib.cw__foo_setmsg(cself, 'hello')
- 这一步直接调用底层的 C 函数,参数需要是符合 C 类型的。
3. 封装在 Python 类里隐藏底层细节
- Djinni 生成的 Python 类会把直接调用封装起来,暴露给用户更自然、Pythonic 的接口。
例如:
foo.setmsg('hello')
- 用户不需要关心
cself
或lib
,只用像调用普通方法一样调用即可。
4. 总结
步骤 | 说明 |
---|---|
1. 导入模块 | from foo_cffi import lib |
2. 调用 C 函数 | lib.cw__foo_setmsg(cself, 'hello') |
3. Python 类封装调用 | 通过 foo.setmsg('hello') 方式调用 |
在多语言互操作中,基础桥接(Basic Bridging) 的几个关键问题,特别是 Python 和 C++ 之间的调用与数据传递。
Step 3: Basic Bridging — Python ↔ C++ 互调基础
1. Python 调用 C++(Python → C++)
- 通过 CFFI 调用 C++ 代码:
Python 调用 C 包装器(wrapper)函数,包装器函数内部调用 C++ 实现。 - 调用流程:
Python → CFFI → C wrapper → C++ 实现 - 示例:
Python 调用lib.cw__foo_setmsg(cself, "hello")
,调用到 C++ 对象的对应方法。
2. C++ 调用 Python(C++ → Python)
- 回调机制:
C++ 端持有指向 Python 对象的引用(通常用 PyObject* 或通过 CFFI 支持的代理) - 调用时通过 CFFI 调用 Python 函数,或通过 Python/C API 调用 Python 方法。
- Djinni 支持跨语言引用:C++ 保存一个代理对象,可以直接调用 Python 实现的接口方法。
3. 数据传递(How do you pass data?)
- Djinni IDL 定义的数据结构映射到 Python 和 C++ 的对应类型
- 数据按值传递(copy),确保语言边界安全
- 基础类型(int、float、string)直接转换
- 复杂类型(record/struct)在两边生成对应类,进行字段逐一复制
- 容器类型(list、set、map)通过对应的 Python 容器和 C++ 容器转换
- Optionals 用 None/nullopt 对应
4. 跨语言对象引用(How do you reference an object from the other language?)
- 智能指针/代理模式:
C++ 持有 Python 对象代理,Python 持有 C++ 对象代理 - Djinni 生成的接口类型都是引用语义,通过指针或句柄在两边保持一致
- 引用计数管理生命周期,防止跨语言对象提前销毁
- 调用方法时,代理负责在语言边界进行转发
总结表格
问题 | 解决方案 |
---|---|
Python → C++ 调用 | CFFI 调用 C 包装器,再转到 C++ |
C++ → Python 调用 | C++ 持有 Python 对象引用,通过 CFFI 或 Python/C API 调用 |
数据传递 | 基础类型和容器映射,按值复制,安全跨语言传递 |
跨语言对象引用 | 代理对象和智能指针,引用计数管理生命周期 |
CFFI 桥接中,Python 调用 C++ 的简洁用法,核心步骤就是:
CFFI Calls: Python → C++ — 简单调用示例
1. 前提
- 先通过 CFFI 在构建时生成并编译好了 Python 的扩展模块(如
foo_cffi.so
)。 - 动态库中有 C 包装器函数,比如
cw__foo_setmsg
,对应调用 C++ 方法。
2. 运行时调用
- 在 Python 里导入绑定模块:
from foo_cffi import lib
- 直接调用包装函数:
lib.cw__foo_setmsg(cself, 'hello')
这里:
lib
是 CFFI 绑定模块里的接口集合cw__foo_setmsg
是 C 包装器函数,负责把调用转发给 C++ 对象方法cself
是 C++ 对象对应的指针或句柄(通常由 Djinni 管理)
3. 总结
步骤 | 说明 |
---|---|
构建时准备 | C++ + C wrapper 编译成动态库,CFFI 生成扩展 |
运行时调用 | Python 导入模块,调用 lib.cw__foo_setmsg |
参数 | cself 代表对象,后面是函数参数 |
效果 | 直接调用到底层 C++ 实现 |
C++ 调用 Python 的桥接实现方式,这里关键点是 C++ 不能直接调用 CFFI,所以用 Python 回调函数传给 C++,C++ 用函数指针保存调用。
CFFI Calls: C++ → Python — 关键流程解析
1. 问题
- C++ 不能直接调用 Python 函数,也不能直接操作 CFFI 生成的 Python 对象。
- 需要“桥梁”让 C++ 调用 Python 代码。
2. 解决方案:Python 创建回调函数
- Python 利用 CFFI 的
@ffi.callback
装饰器定义一个 C 函数指针类型的回调函数:
@ffi.callback('void(const char *)')
def log(msg):print(msg.decode('utf-8'))
- 这个回调函数看起来是 C 函数指针,实际调用时会转到 Python 代码。
3. 把回调传递给 C++
- Python 调用暴露的 C 包装器函数,把回调指针传给 C++ 端:
lib.cw__foo_addcallback_log(log)
- C++ 端存储这个函数指针,后续需要调用 Python 函数时直接调用这个指针。
4. 回调调用示例
- 当 C++ 需要触发回调时,调用保存的函数指针:
// C++ 伪代码
typedef void (*LogCallback)(const char *);
LogCallback log_callback;
void cw__foo_addcallback_log(LogCallback cb) {log_callback = cb;
}
void some_function() {if (log_callback) {log_callback("Hello from C++");}
}
5. 整个流程总结
步骤 | 说明 |
---|---|
Python 定义回调函数 | 用 @ffi.callback 定义 C 函数指针回调 |
传递回调到 C++ | 调用包装函数,将回调指针传给 C++ |
C++ 存储回调函数指针 | 用于后续调用 |
C++ 调用回调触发 Python | C++ 调用函数指针,实际上调用 Python 回调函数 |
CFFI 处理数据类型时的关键点,特别是和 Djinni 结合时的数据交互方式,我帮你总结一下:
CFFI Data — 数据类型和处理
1. CFFI 支持的类型
- 基本类型(primitives):
布尔值(bool)、整型(int, long)、浮点型(float, double)等,直接映射。 - 数组(arrays):
可以传递定长或变长数组,或指向数组的指针。 - 结构体(structs):
CFFI 可以声明结构体,支持字段访问。 - 指针(pointers):
可以传递指向数据的指针,也可以是指向不透明结构体(opaque structs)。
2. Djinni 使用方式
- 指向不透明结构体的指针:
对于复杂对象,CFFI 只声明结构体类型,但不定义其内部(opaque struct)。
这样,Python 端不知道结构体具体内容,只能通过指针操作。 - 结构体创建和操作在 C++ 端完成:
Python 侧只通过指针引用这些对象,不直接操作内部数据。 - 只在 C++ 侧定义包装结构体(wrapper structs),Python 侧只知类型。
3. 数据管理策略
- Python 通过 CFFI 调用 C++ 的接口操作数据
- 数据结构由 C++ 管理生命周期和具体实现细节
- Python 端透明地持有对象指针,调用对应方法操作数据
4. 总结
类型类别 | CFFI 支持情况 | Djinni 中应用 |
---|---|---|
基本类型 | 直接映射 | 直接传递 |
数组 | 支持指针和数组 | 用于容器或字段 |
结构体 | 声明和访问字段 | Python 端声明,C++ 端定义 |
不透明结构体指针 | 只声明类型,不定义内容 | 用作对象引用 |
生命周期管理 | 在 C++ 端管理 | Python 只持指针,透明使用 |
跨语言对象引用时,C++ 对象如何传递给 Python,具体用法是:
Objects: C++ → Python — 通过不透明结构体指针传递
关键点
- C++ 对象包装成不透明结构体(opaque struct)
Python 端只知道指针类型,不知道具体实现细节。 - 把 C++ 对象指针传递给 Python
Python 通过 CFFI 接收为cdata
对象。 - Python 使用这个
cdata
指针来调用对应的包装函数,间接操作 C++ 对象。
过程示例
// C++ 定义不透明类型
struct MyObject { void do_something();
};
extern "C" MyObject* create_my_object();
extern "C" void my_object_do_something(MyObject* obj);
# Python 侧通过 CFFI 得到 MyObject* 指针,作为 cdata 使用
obj = lib.create_my_object() # obj 是 cdata 指针
lib.my_object_do_something(obj) # 调用对应函数操作对象
总结
步骤 | 说明 |
---|---|
C++ 定义对象及函数接口 | 只声明指针类型,不暴露内部细节 |
传递指针给 Python | 作为不透明指针,Python 只持有 cdata |
Python 通过包装函数调用 | 通过指针调用 C++ 函数操作对象 |
Python 对象传递给 C++ 的机制,具体是用 CFFI 的 ffi.new_handle()
和 ffi.from_handle()
来封装和恢复 Python 对象指针。
Objects: Python → C++ — 用 ffi.new_handle 和 ffi.from_handle 传递对象
1. 创建句柄(Handle)
- 在 Python 里用
ffi.new_handle(obj)
- 生成一个
cdata
指针,内部封装了 Python 对象obj
- 这个指针可以安全地传递给 C++,类型是
void *
或自定义指针类型
- 生成一个
2. 传递给 C++
- Python 把
cdata
指针传给 C++(通常作为void*
接收) - C++ 端仅保存指针,可能传递给回调或保存使用
3. 从指针恢复 Python 对象
- C++ 通过回调或接口把指针传回 Python
- Python 用
ffi.from_handle(ptr)
恢复原始的 Python 对象引用 - 这样就能操作原始 Python 对象了
4. 对象生命周期管理
- 这个过程中需要特别注意对象生命周期:
new_handle
会增加对象引用计数,防止被提前销毁- 但需要在适当时机释放,防止内存泄漏
- 你提到“对象生命周期很复杂”,后续可以详细讲这个部分
流程简图
Python obj↓ ffi.new_handle(obj)
cdata ptr (void*)↓ 传递给 C++
C++ 保存 ptr↓ 传回 Python
Python ffi.from_handle(ptr)↓
原始 Python obj 恢复
Step 4: Bridging Features,在完成了基础的类型映射和跨语言调用后,Djinni 需要在这些基础之上实现更高级的功能。总结如下:
Step 4: Bridging Features — 构建在基础之上的核心功能
1. Proxies for Interfaces(接口代理)
- 生成跨语言的接口代理类
- 允许调用方像调用本地对象一样调用远端实现
- 代理负责跨语言调用的转发和参数转换
2. Marshaling Structured Data(结构化数据的编组/传输)
- 支持 record/struct 类型的数据自动转换
- 支持容器类型(list、set、map)递归转换
- 确保数据安全按值传递,不共享内存避免并发风险
3. Object Ownership Across Languages(跨语言对象所有权管理)
- 跨语言对象引用计数或智能指针管理
- 保证对象生命周期一致,防止悬空指针或内存泄漏
- 支持弱引用,避免循环依赖
4. Handling Exceptions(异常处理)
- 异常从一个语言抛出后正确传递到另一语言
- 映射不同语言的异常类型
- 保证跨语言调用时异常不丢失,方便调试和错误处理
总结
功能 | 作用与意义 |
---|---|
接口代理(Proxies) | 跨语言无缝调用接口,实现方法转发 |
结构化数据编组 | 自动处理复杂数据类型,保证数据完整性 |
对象所有权管理 | 管理对象生命周期,避免资源管理问题 |
异常处理 | 跨语言异常传递,保证程序健壮性 |
Proxy Objects 是跨语言桥接的关键概念,我帮你整理总结一下:
Proxy Objects — 跨语言接口代理对象
1. 定义
- Proxy(代理) 是在语言 A 中的对象,它代表(stand in for)语言 B 中的真实对象。
- 代理在语言 A 里看起来就是本地对象,实际所有方法调用都转发到语言 B。
2. 双向代理
- 每个接口通常都有两个代理版本,分别在两个语言环境中使用。
- 例如:
MyModelCppProxy
:Python 里的代理,持有一个 C++ 端的MyModel
实例引用。- 相反的,C++ 也可能有一个代理持有 Python 实现。
3. 工作原理
- Python 端的
MyModel
对象是一个代理,内部持有指向 C++MyModel
对象的指针。 - 当调用
MyModel.do_stuff(info)
,实际转发给 C++ 端的do_stuff
方法。 - 代理负责参数转换、调用转发、结果返回。
4. 示例
class MyModel:def __init__(self, cxx_obj_ptr):self._cxx_obj_ptr = cxx_obj_ptrdef do_stuff(self, info):# 调用 C++ 代理函数,传递 self._cxx_obj_ptr 和参数lib.cw__my_model_do_stuff(self._cxx_obj_ptr, info)
5. 总结
概念 | 说明 |
---|---|
Proxy | 语言 A 中代表语言 B 对象的代理对象 |
双向代理 | 每种语言都可持有对另一语言对象的代理 |
方法调用转发 | 代理负责跨语言方法调用和数据转换 |
生命周期管理 | 代理持有对远端对象的引用,管理生命周期 |
Marshaling Structured Data 的过程,重点是如何高效地逐步构建复杂数据结构,同时避免不必要的拷贝。
Marshaling Structured Data — 结构化数据的编组
1. 数据构建步骤示例
给定两个 record:
record record1 {s: string;
}
record record2 {list1: list<record1>;
}
构建过程:
- 先构建字符串
例:"hello"
- 构建
record1
实例,字段s
指向该字符串
例如:record1_instance = { s: "hello" }
- 把多个
record1
放进列表list1
例如:list1 = [record1_instance, ...]
- 构建
record2
,字段list1
指向该列表
例如:record2_instance = { list1: list1 }
2. 避免重复拷贝
- 数据构建时避免在每一步都复制数据
- 使用引用传递或智能指针管理底层数据
- 只有在真正跨语言传输或持久化时才做深拷贝(marshal)
3. 总结
过程步骤 | 说明 |
---|---|
构建字符串 | 先生成最底层基础数据 |
构建 record1 | 以引用或指针关联字符串 |
构建列表 | 列表持有 record1 对象引用 |
构建 record2 | record2 持有列表的引用 |
避免每步复制数据 | 通过引用/指针传递实现高效构建 |
这样构建数据结构,保证了性能又符合 Djinni 的数据传递设计。 |
如何在跨语言 marshaling 过程中最大限度减少数据拷贝,主要通过 C++ 来控制序列化细节,同时配合 Python 的灵活构造。
如何最小化拷贝?——跨语言 marshaling 优化策略
1. C++ 控制 marshaling 过程
- C++ 端主导数据封装与拆解
- 利用 C++ 的 按值传递 和 move 语义,避免不必要的深拷贝
- 只在真正需要时复制数据
2. Python 端允许增量构造
- Python 对象字段可逐步赋值,方便分步构建复杂对象
- 这让 C++ 只需调用 Python 子结构的构造代码,避免整体重建
3. 双向完整对象 marshaling
- 生成的 C++ 代码能将整个对象(record)完整序列化或反序列化
- 方向双向支持:C++ → Python,Python → C++ 都有对应的 marshal 函数
4. 辅助回调函数
- 代码生成器为每个复杂字段生成辅助回调(helper callbacks)
- 这些回调负责子对象的构建与传递
- 保证拆解和组合过程中无冗余拷贝
5. 总结
方法 | 目的 |
---|---|
C++ 利用 move 语义 | 避免不必要的内存复制 |
Python 支持增量字段赋值 | 灵活构造复杂对象 |
生成完整 marshal 代码 | 简化整体序列化流程 |
辅助回调函数 | 分步构建子结构,高效复用 |
Python 到 C++ 传递复杂对象时的细节:
Python → C++ 数据传递细节
1. Python 传递整个对象引用
- Python 侧把完整的对象引用(Python 对象指针)传给 C++
- C++ 不直接拿到全部数据,只拿到对象句柄
2. C++ 按需请求子字段
- C++ 通过调用接口或回调,逐个字段请求数据
- 对于 record,每个字段单独调用一次
- 对于容器,可能以迭代器风格多次调用获取元素
3. C++ 一次性构造完整对象
- 收集所有字段数据后,C++ 执行一次完整的构造
- 利用移动语义(move)或返回值优化(RVO)避免拷贝
4. 总结
过程步骤 | 描述 |
---|---|
Python 传引用 | 传递对象指针,不复制数据 |
C++ 逐字段请求 | 一次调用获取一个字段数据 |
容器元素迭代请求 | 多次调用获取容器内各元素 |
一次构造对象 | 使用 move/RVO 避免多余复制 |
C++ 调用 Python 构造复杂对象时的流程,重点在于增量构建和按引用传递,避免拷贝。总结如下:
C++ → Python 数据传递细节
1. C++ 请求 Python 创建空对象
- Python 提供一个接口,返回一个空的对象实例(例如空的 record 类实例)
2. C++ 按字段或元素逐个填充
- C++ 调用 Python 对象的方法或属性,动态添加字段或向容器(list/dict/set)添加元素
- 逐步构造复杂数据结构
3. 完成后传递完整对象
- 填充完成后,将构建好的 Python 对象完整传回调用方
- 由于 Python 对象本身是引用类型,不涉及数据复制
4. 总结
步骤 | 说明 |
---|---|
Python 创建空对象 | 返回空实例供 C++ 填充 |
C++ 动态赋值字段或添加元素 | 分步填充,支持容器动态增长 |
返回完整对象 | Python 引用传递,无额外复制 |
跨语言桥接中关于对象所有权管理的核心原则,关键点如下:
Object Ownership — 跨语言对象所有权
1. 跨语言无法直接表达所有权
- C/C++ 的类型系统(尤其是 C 接口)无法直接表达所有权转移
- 所以跨语言传递对象时,必须有明确的所有权约定和管理策略
2. 我们的黄金规则
“一个对象跨语言边界传递时,所有权随之转移”
- 意味着:当你把一个对象从语言 A 传到语言 B,语言 B 负责管理该对象的生命周期
- 这避免了双重释放或内存泄漏
3. 特例
- 某些内部辅助类型(如缓存、代理对象)可能有特殊的所有权约定
- 但整体设计以所有权转移为主线
4. 总结
事实 | 说明 |
---|---|
C 类型不表达所有权 | 只能用约定和智能指针管理 |
跨语言传递即所有权转移 | 传递对象即转移管理责任 |
特殊内部辅助类型除外 | 有例外但有限 |
Implementing Ownership — 实现对象所有权
1. 显式释放所有权
- 对象所有权传递后,必须显式调用
delete
或等效函数释放资源 - 防止内存泄漏,保证生命周期管理清晰
2. RAII 自动管理
- C++ 使用 RAII(资源获取即初始化)原则管理生命周期
- 通过智能指针(如
unique_ptr
)封装资源,确保超出作用域时自动释放
3. 自定义删除器(Custom Deleters)
- 对于跨语言对象,
unique_ptr
搭配自定义删除器处理复杂释放逻辑 - 例如调用跨语言的析构回调或释放函数
4. Python 端辅助管理
- Python 端通过上下文管理器 (
with
语句块) 简化资源管理 - 自动调用析构或清理函数,配合 C++ 智能指针实现跨语言所有权安全
5. 总结
技术点 | 作用 |
---|---|
显式删除 | 明确释放跨语言传递的对象 |
RAII | 自动管理生命周期,防止泄漏 |
unique_ptr + 删除器 | 结合定制释放,支持复杂所有权管理 |
Python with 块 | 简化 Python 端资源清理 |
Python 和 C++ 跨语言所有权管理中,Python 垃圾回收(GC)和 C++ 资源释放的协调问题,重点是延迟 Python 对象回收直到 C++ 显式释放。
Explicit Ownership in Python — Python 中的显式所有权管理
1. Python GC 与 C++ 对象生命周期冲突
- Python 垃圾回收会自动删除不再使用的对象
- 但如果该对象已经传给 C++,提前回收会导致 C++ 访问悬挂指针
2. 延迟 GC,确保 C++ 先释放
- 需要保证 Python 对象在 C++ 显式删除之前保持存活
3. 解决方案:全局引用池
- 使用 Python 端一个全局的
c_data_set
容器,存放ffi.new_handle(obj)
产生的 c_data 对象 - 这个全局容器保持对 Python 对象的强引用,阻止 GC 回收
4. 释放机制
- 当 C++ 调用删除回调时,Python 端从
c_data_set
中移除对应引用 - 这样 Python GC 才能真正回收该对象
5. 工作流程示意
步骤 | 描述 |
---|---|
Python 创建对象并调用 ffi.new_handle(obj) | 生成跨语言指针并存入全局集合 |
Python 传指针给 C++ | C++ 持有 void* 指针使用该对象 |
C++ 显式删除对象 | 触发删除回调 |
Python 删除全局集合中的引用 | 允许 GC 回收该 Python 对象 |
Exclusive Ownership(独占所有权) 在跨语言调用中的关键模式,特别是 C++ 和 Python 之间通过 Djinni 桥接时的对象生命周期管理。以下是重点总结:
Exclusive Ownership(独占所有权)
1. 大多数对象是“独占”地跨语言传递的
- 意思是:一个对象一旦传给另一边(比如从 Python 到 C++),只有目标语言拥有它的所有权
- 原始语言(调用方)不再保留所有权责任
2. 需要显式 delete()
回调
- 一旦 C++ 拿到一个 Python 对象(例如用
ffi.new_handle(obj)
传过去)
-> Python 必须等到 C++ 回调确认“我不再需要这个对象”之后,才能释放它 - 所以会设置一个 deleter callback,在适当的时候释放资源
3. C 端包装结构:只在 Python 临时持有
这些结构体只是桥梁、临时中转用,Python 是临时拥有者:
类型 | 用途 |
---|---|
DjinniString | 表示字符串 |
DjinniBinary | 表示二进制数据(如 bytes ) |
BoxedI32 | 包装整型(int32) |
BoxedF64 | 包装浮点(float64) |
这些类型通常由 Python 创建并传给 C++,一旦用完会被销毁。 |
4. Python 对象被 C++ 拿走(via handle)
- 使用
ffi.new_handle(obj)
生成的句柄,传递给 C++ - Python 保持该对象活着(存入
c_data_set
) - C++ 使用完后,通过回调告诉 Python“你可以删掉这个对象了”
总结表格
跨语言对象 | 谁持有所有权? | 释放方式 |
---|---|---|
DjinniString 等包装类型 | Python(短暂) | Python 用完即删 |
Python 对象 | C++(通过 handle 接收) | C++ 触发回调,Python 再释放 |
C++ 对象 | Python(持有 cdata 指针) | Python 调 deleter 回调删除 C++ |
Shared Ownership(共享所有权),主要用于接口对象(比如跨语言的 MyModel
)因为它们需要在 C++ 和 Python 之间多次传递,所以不能使用独占所有权(即不能一传过去就销毁)。下面是详细解释:
Shared Ownership(共享所有权)
为什么需要共享所有权?
- 有些对象需要在多个语言之间来回传递
- 比如一个跨平台的数据模型
MyModel
:- 它可能最初由 Python 创建
- 然后传给 C++
- 然后又传回 Python,再传给其他 C++ 组件
- 所以这些对象不能在某一边直接“拥有”和释放
- 必须使用 引用计数(ref-counting) 来安全管理生命周期
关键组件解释
组件 | 作用 |
---|---|
DjinniWrapperMyModel (C 包装结构) | 由 Python 持有,带有自己的引用计数(用于 Python 持有的引用) |
shared_ptr<MyModel> | 在 C++ 中持有真正对象或代理,自动引用计数 |
PythonProxy | 若 C++ 持有的是一个 Python 实现的对象,它就是一个代理对象 |
handle | PythonProxy 内部唯一持有 Python 对象(通过 ffi.new_handle() ) |
引用计数操作
inc_ref()
:每次一个语言接收这个对象,调用一次dec_ref()
:当不再使用时,减少引用- 当引用计数为 0,才释放内存(无论在哪个语言)
生命周期关系图(概念)
Python → C++ (shared_ptr<MyModel>)↑ ↓
[handle] ← PythonProxy ← DjinniWrapperMyModel↑ref count (inc/dec)
总结逻辑
- Python 使用
DjinniWrapperMyModel
,它带一个本地引用计数 - C++ 使用
shared_ptr<MyModel>
,它可能指向:- 真正的 C++ 实现
- 或者是一个
PythonProxy
(代理对象)
PythonProxy
独占拥有 Python 的句柄 (ffi.new_handle(obj)
)- 双方引用计数配合,保证对象直到没人用才被销毁
异常传播(Propagating Exceptions) 的处理机制,尤其是在使用 CFFI 桥接 C++ 与 Python 时,如何正确、安全地在不同语言之间传递异常。因为 C 本身不支持异常机制,这部分是桥接代码里非常关键的一环。
异常传播的核心问题
默认行为(不处理时)
- CFFI 默认不会传递异常(因为 C 没有异常机制)
- 如果 Python 抛出异常,C++ 代码无法感知,会:
- 忽略异常(不安全)
- 或者 直接崩溃(很危险)
Djinni 异常传播的做法
1. 桥接代码捕获异常
- 所有从 Python → C++ 或 C++ → Python 的调用都包裹在
try-catch
块中
2. 异常保存:用线程局部存储
- 使用 thread-local state(TLS) 保存异常对象或状态
- 和
errno
(C)或GetLastError()
(Windows)的机制类似 - 每个线程有自己独立的异常状态
- 和
3. 异常封送(marshal)
- 把异常从 Python 封装为 C 能理解的结构(比如:字符串、类型码、句柄等)
- 跨语言传输后再解包,还原成目标语言的异常对象
4. 重新抛出(Re-throw)
- 在调用链的另一端,由生成的 Djinni 桥接代码判断:
- 如果 TLS 中存储了异常 → 抛出对应语言的异常
- 否则正常返回
为什么这样做是安全的?
- 每个线程有独立异常状态,不会相互干扰
- 所有生成的桥接代码都自动检测和处理异常,不会遗漏
- 避免了语言间“沉默的错误”或“未定义行为”的风险
示意流程图
Python 调用 C++ 方法
↓
CFFI 桥接层 (Python → C)
↓ try:C++ 方法抛异常
↓ catch:保存异常到 TLS返回错误码
↓
Djinni Python 端检测到错误
↓
从 TLS 读取异常
→ Python re-raise()
总结
步骤 | 说明 |
---|---|
异常捕获 | 桥接代码在边界处 try/catch |
异常保存 | 存在 per-thread 状态中 |
异常封送 | 变成可以跨语言传递的数据结构 |
异常重抛 | 目标语言(Python 或 C++)再抛出原始异常 |
Djinni 在实现跨语言调用时的 异常状态管理机制,特别是在 Python 和 C++ 之间使用 CFFI 桥接时,如何在两边正确、安全地传递异常信息。这里强调的是:
Exception State(异常状态)
概念总结
项目 | 说明 |
---|---|
Per-thread variable | 每个线程都有独立的异常状态,避免线程间冲突 |
Contains Python exception | 存储的是 Python 抛出的异常对象,用 ffi.new_handle() 包裹 |
Held in C++ | 异常对象的句柄存在 C++ 中,作为 void* 类型保存 |
Only populated while crossing boundary | 只有在 Python ↔ C++ 调用发生时设置,不会持久存在或干扰常规运行 |
Checked and cleared after each call | 每次跨语言调用结束后都自动检查和清理(防止遗留异常) |
Symmetric code | 双向调用都遵循这个流程:Python → C++,C++ → Python 都一样 |
使用流程(简化)
- Python 调 C++:
- Python 调用 C 函数 → C++ 抛异常
- 异常被捕获,并存入 线程本地异常状态
- Python 回到桥接代码,检测到异常 → 使用
ffi.from_handle()
恢复 → 抛出原始 Python 异常
- C++ 调 Python:
- Python 注册回调传入 C++(作为
void*
) - C++ 调用 Python 函数 → Python 抛异常
- Python 端的桥接函数捕获异常 → 存入线程本地异常状态(通过 C 接口)
- C++ 检查返回状态 → 显式处理或 re-throw
- Python 注册回调传入 C++(作为
示例结构(伪代码)
C++ 侧
// Per-thread exception holder
thread_local PyObjectHandle* g_thread_exception = nullptr;
void store_exception(PyObjectHandle* ex) {g_thread_exception = ex;
}
PyObjectHandle* take_exception() {auto ex = g_thread_exception;g_thread_exception = nullptr;return ex;
}
Python 侧(桥接)
@ffi.callback("void*()")
def python_function():try:# user logicexcept Exception as e:handle = ffi.new_handle(e)lib.store_exception(handle)return ffi.NULL
安全保障
- 不丢异常:每次调用都自动检查异常状态
- 不残留状态:异常总是调用后立即清理,避免污染下一次调用
- 线程安全:线程局部变量确保并发环境下不互相干扰
- 对称性好维护:Python→C++ 和 C++→Python 的处理机制相似
异常处理的职责分工(Exception Responsibilities),这是 Djinni 跨语言异常机制中的核心执行流程。它明确了 调用者(Caller) 和 被调用者(Callee) 各自需要做什么,以确保异常能够正确跨语言边界传递、捕获和处理。
异常处理的职责分工
被调用者(Callee)的责任
- 在函数实现外围加
try/catch
- 无论是 Python 还是 C++ 实现方,都必须把自己的逻辑用
try/catch
包裹
- 无论是 Python 还是 C++ 实现方,都必须把自己的逻辑用
- 在
catch
里:- 捕获到异常后,调用特定接口将异常对象写入“线程异常状态”
- 然后返回一个“出错标志”或空指针/null(不能直接抛异常,因为桥接代码无法识别)
关键点:
异常不能直接跨语言抛出,必须转化为中间状态保存
调用者(Caller)的责任
- 在调用完成后立即检查异常状态
- 调用之后,必须查询“线程异常状态”是否有异常被设置
- 如果有异常:
- 先清除异常状态(防止污染下一次调用)
- 然后用目标语言的方式(如 Python 的
raise
或 C++ 的throw
)重新抛出异常
关键点:
如果调用者不检查并清除异常状态,就会导致异常“卡在中间”,程序行为不确定
整体流程总结
阶段 | 步骤 |
---|---|
被调用者 | try { … } catch { set_exception(); return error; } |
调用者 | result = call(); if (check_exception()) { throw; } |
🖼 示例流程图
Caller (Python or C++)
│
├─▶ Call function via CFFI
│ │
│ ├─▶ Callee (Python or C++)
│ │ try {
│ │ ...implementation...
│ │ } catch (e) {
│ │ set_exception_state(e)
│ │ return error/null
│ │ }
│ │
│ ◀──────── return to Caller
│
├─▶ Caller checks exception_state()
│ ├─ if present → clear + throw/re-raise
│ └─ else → use return result
为什么这么设计?
- 保证跨语言环境下异常一致、安全
- 避免在桥接层直接传递 native 异常(C 没有异常机制)
- 让每一层明确自己的职责,易于维护和调试
异常封送(Marshaling Exceptions)* 的机制概览。
这是指在跨语言调用过程中(如 Python ↔ C++,或 Java ↔ C++)如何将一个语言中抛出的异常 转换为另一个语言可理解的异常,从而能在另一端进行正确的处理(比如重新抛出、记录或包装)。
Djinni 的异常封送机制:关键点
默认行为
- Djinni 提供了一个 默认的异常转换机制(simple translation):
- 常见异常(如
std::exception
,RuntimeException
, Python 的Exception
)会被映射成目标语言里的通用异常类型。 - 示例:C++ 的
std::runtime_error("bad")
→ Java 中的RuntimeException("bad")
。
- 常见异常(如
可插拔:支持自定义
- 你可以“插入”你自己的异常转换逻辑:
- 比如你希望 C++ 中的
NetworkError
映射到 Python 中的CustomNetworkException
。 - Djinni 的生成代码留出了钩子接口,允许你扩展异常翻译器。
- 比如你希望 C++ 中的
类似 Java 的模型
- Djinni 在设计 Python 支持时沿用了 Java 支持中的异常处理思路:
- 异常 只在语言边界转换一次,中间不嵌套。
- 异常信息(通常包括类型名 + message)在边界上转换为目标语言的异常对象。
你提到的后续内容(“deep dive later”)
- 可能包括:
- 如何注册自定义异常映射函数
- 如何序列化复杂异常(如带堆栈、错误码)
- 如何处理非标准异常类型
这是对 Djinni Python 支持部分的总结(Python Wrap-up),以下是关键内容的整理:
Python 支持的基础已经建立
基于前面所描述的:
- 类型映射(records, enums, interfaces, primitives)
- 跨语言桥接(使用 CFFI)
- 异常处理与对象生命周期管理
Djinni 的 Python 支持已经有了 完整的最小可用架构(MVP)。
可以在此基础上扩展的内容包括:
手写的 marshaling helpers
- 用于处理复杂或自定义数据结构的序列化/反序列化逻辑
Global proxy cache
- 避免为同一个 C++ 对象创建多个 Python proxy 实例(保持对象恒等)
Derived 操作(比较 / 哈希)
- 自动生成
__eq__
,__hash__
等操作,使 Djinni record/enum 在 Python 中表现得更自然
支持更多语言特性:
- 静态方法 (
@staticmethod
) - 常量值(如
const PI = 3.14
) - …等你希望接口支持的语义增强
注意:Python 支持仍为实验性(experimental)
虽然大多数特性已经实现,但 Djinni 的 Python 后端:
- 仍在开发中(不是主分支)
- 接口和生成代码可能会有 breaking changes
- 适合对工具链掌握较深、有跨语言需求的高级开发者参与和反馈
GitHub 仓库(Python 分支)
你可以访问:
https://github.com/dropbox/djinni/tree/python
查看 Python 相关支持的源代码和构建工具,包括:
- Python 代码生成器
- 示例项目
- CFFI 封装逻辑
- 构建脚本(可能包含
setup.py
,Makefile
, etc.)
Djinni 项目在 Python 支持扩展过程中的 同步进度总结(Sync Progress),主要包括以下几个技术主题:
1. Djinni Overview
简要回顾了 Djinni 的核心目标:
- 用 IDL 定义跨平台接口和数据结构
- 自动生成 C++ / Java / Objective-C / Python 的桥接代码
- 支持多语言交互:每个语言看到的是自然、原生的代码接口
2. Adding a New Language: Python
- 增加 Python 支持是此次工作重点
- 使用 CFFI 技术实现 Python 与 C++ 的桥接
- 解决了跨语言类型表示、对象生命周期、异常传播等问题
3. Implementation Techniques
涉及 Djinni 实现的一些高级技巧和扩展内容:
Proxy Caching for Identity Semantics
- 解决 对象恒等性问题(Object identity):
- 避免重复创建 Proxy,使多次桥接返回的对象具有相同身份(如
a is b
为True
)
- 避免重复创建 Proxy,使多次桥接返回的对象具有相同身份(如
- 通过 全局缓存 保存 proxy 映射(通常用
weakref.WeakValueDictionary
)
Nullability
- 处理各语言间对 null / None / nullptr 的不同处理方式
- Djinni 支持在 IDL 中标记字段为可选(
optional<T>
) - Python 中使用
None
表示空值,自动处理可选值的 marshaling/unmarshaling
Customized Java Exception Translation
- Java 端的异常翻译支持 自定义映射
- C++ → Java 抛出
MyCppException
,可映射为 Java 的MyCustomException
- C++ → Java 抛出
- 可根据异常类型和内容做逻辑判断、重新包装异常、加日志等
这是关于 Djinni 中 Proxy Objects 的实现机制 的进一步解释,重点在于接口跨语言调用的生成方式,以及如何借助代理对象进行桥接。
Proxy Objects(代理对象)概念
Djinni 为每个语言:
- 生成接口定义(interface stub)
- 如
weather_listener
、weather_service
- 如
- 生成实现类(Proxy 类)
- 负责将方法调用跨语言转发
Java ↔ C++ 示例
Java 调用 C++ 的流程:
// Java 接口
interface Widget {void foo();
}
// Java 实现调用 native
public class WidgetCppProxy implements Widget {private native void foo(); // native 声明@Overridepublic void foo() {foo(); // 调用 native 方法}
}
// C++ 生成的 JNI 实现
JNIEXPORT void JNICALL Java_pkg_Widget_foo(JNIEnv* env, jobject obj) {// C++ 逻辑实现
}
C++ 调用 Java 的流程:
使用 interface +j +o 生成 Java 接口代理
weather_listener = interface +j +o {weather_report(date: date, forecast: weather);
};
Djinni 会生成:
- Java 接口
WeatherListener
- C++ 代理类
WeatherListenerCppProxy
- JNI 桥接代码将 C++ 调用转发给 Java 实例
这样你在 C++ 中写:
listener->weather_report(date, forecast); // 实际进入 Java 的实现
更完整的应用场景(发布-订阅):
weather_service = interface +c {add_listener(listener: weather_listener);remove_listener(listener: weather_listener);
};
使用场景:
- Java/ObjC 注册监听器(实现
weather_listener
) - C++
weather_service
保存这些监听器代理 - 当天气数据到达,C++ 主动调用监听器方法,自动转发到 Java/ObjC 层
核心要点总结
概念 | 说明 |
---|---|
Proxy Object | 一种自动生成的跨语言代理,实现接口调用转发 |
Interface +j / +o | 表示目标接口将在 Java / Objective-C 中实现 |
Interface +c | 表示目标接口将在 C++ 中实现(供其它语言调用) |
Proxy 调用方向 | Java → C++ 使用 JNI C++ → Java 使用 JavaVM 接口和反射桥接 |
这是一个用 C++ 实现的 Djinni 接口 WeatherService
的具体类 MyWeatherService
,用于管理跨语言的 WeatherListener
回调对象。下面是详细解析:
class MyWeatherService : public WeatherService
{
public: void add_listener(const shared_ptr<WeatherListener> & listener) { m_listeners.insert(listener); } void remove_listener(const shared_ptr<WeatherListener> & listener) { m_listeners.erase(listener); }
private: set<shared_ptr<WeatherListener>> m_listeners;
};
``
## 类功能说明
```cpp
class MyWeatherService : public WeatherService
这表示你实现了 Djinni 中定义的接口:
weather_service = interface +c {add_listener(listener: weather_listener);remove_listener(listener: weather_listener);
};
生成的 C++ 接口:
class WeatherService {
public:virtual void add_listener(const std::shared_ptr<WeatherListener>& listener) = 0;virtual void remove_listener(const std::shared_ptr<WeatherListener>& listener) = 0;
};
成员方法解释
void add_listener(const shared_ptr<WeatherListener> & listener)
- 添加监听器
listener
是跨语言传入的回调对象(Java/ObjC 实现)- 通过 Djinni 的桥接被包装为
shared_ptr<WeatherListener>
void remove_listener(const shared_ptr<WeatherListener> & listener)
- 移除监听器
- 注意:这依赖于
shared_ptr
的 值相等性,即引用的是同一个监听器对象。
成员变量
set<shared_ptr<WeatherListener>> m_listeners;
- 用
std::set
保证监听器唯一(不会重复添加) - 存储所有监听器回调
- 可通过遍历调用
listener->weather_report(...)
向每个语言层发通知
跨语言监听器交互场景
- Java 注册监听器:
weatherService.addListener(new WeatherListener() {public void weatherReport(Date date, Weather forecast) {System.out.println("Received forecast!");}
});
- Djinni 自动生成 Proxy,将该 Java 对象包装为
shared_ptr<WeatherListener>
- C++ 中的
MyWeatherService::add_listener()
被调用,添加监听器 - 当新天气数据到达时,C++ 代码主动调用所有监听器:
for (auto &listener : m_listeners) {listener->weather_report(date, forecast);
}
Djinni 桥接将调用转发到 Java 实现
☑ 总结
组件 | 说明 |
---|---|
WeatherService | Djinni 定义的服务接口(C++ 实现) |
WeatherListener | Djinni 定义的回调接口(Java/ObjC 实现) |
MyWeatherService | C++ 服务实现,调用 listener 跨语言通知 |
shared_ptr<T> | 用于自动管理对象生命周期,防止内存泄漏 |
Djinni Proxy | 自动生成桥接代码,把语言间对象互相包装和调用 |
Proxy Caching(代理缓存) 是解决跨语言桥接中对象身份(identity)保持一致性的问题,具体解析如下:
问题背景
- 在跨语言调用时,同一个接口对象(例如
WeatherListener
)从 Java/Python 传给 C++ 会被封装成一个新的 Proxy 对象(shared_ptr<WeatherListener>
), - 如果不缓存代理,重复传入同一对象会生成多个不同的代理实例。
set::erase
、set::find
等容器操作依赖对象身份,若用不同的 Proxy 实例,会导致删除失败或查找失败。- 除了 correctness,缓存还能带来性能提升,减少重复构造代理对象。
解决方案
1. 代理缓存机制:
- 维护一个映射:
key 是对“语言A端对象”的非拥有(non-owning)引用(比如 C++ 侧的裸指针、或 Python 中的 id) - value 是代理对象的弱指针(weak_ptr):
如果缓存中的代理还活着,直接复用;否则重新创建新的代理对象并缓存。
2. 缓存作用
作用 | 说明 |
---|---|
保持对象身份一致 | 保证同一个跨语言对象总对应同一个代理 |
容器操作正确 | 使 set::erase 、find 有效 |
降低开销 | 避免重复创建销毁代理对象 |
3. 简单示意(伪代码)
// 全局缓存,key 是对语言A对象的引用,value 是 Proxy 弱指针
std::unordered_map<RawObjectRef, std::weak_ptr<WeatherListenerProxy>> proxy_cache;
std::shared_ptr<WeatherListenerProxy> get_or_create_proxy(RawObjectRef obj_ref) {auto it = proxy_cache.find(obj_ref);if (it != proxy_cache.end()) {if (auto cached_proxy = it->second.lock()) {return cached_proxy; // 重用已有代理}}// 创建新代理并缓存auto new_proxy = std::make_shared<WeatherListenerProxy>(obj_ref);proxy_cache[obj_ref] = new_proxy;return new_proxy;
}
4. 总结
- proxy caching 是跨语言桥接保证对象身份一致性的重要技术
- 缓存代理,避免多次包装同一个对象
- 解决语言边界上复杂的等价比较、容器操作等问题
不变量 (Invariants) 是代理缓存设计的核心原则,详细解释如下:
不变量说明
1. 保证:
“不同时刻不会有两个不同的代理对象同时‘可见’(visible)给调用者”
- visible(可见):当前程序逻辑或接口层面能访问到的代理实例
- 不存在同时存在两个不同的代理包装同一个底层对象的情况
这保证了: - 对象身份一致性(identity consistency)
- 容器操作(如
set::erase
)不会失败 - 语义上等价的对象具有相同代理
2. 不是 LRU 缓存
- 不做最近最少使用(LRU)策略
- 代理不会因为不活跃自动立刻被删除,仅由弱指针生命周期决定
3. 代理生命周期
- Wrappers don’t last forever
代理对象有生命周期,会被垃圾回收或销毁(C++ shared_ptr 计数为0) - Visible, not in existence
代理只有在被引用时才“存在”- 当代理弱引用(weak_ptr)失效(没有任何 shared_ptr 持有)时,下次请求同一对象时,会新建代理
4. 通过弱指针过期控制代理重建
- 弱指针(weak_ptr)用于检测代理对象是否还存活
- 代理死亡后,缓存里对应 weak_ptr 失效
- 下一次请求时重新创建代理
总结
内容 | 说明 |
---|---|
不允许重复代理 | 同一时刻最多有一个代理实例对应同一个跨语言对象 |
生命周期受限 | 代理生命周期由引用计数控制,超出作用域自动销毁 |
缓存非永久 | 缓存中的代理只是 weak_ptr,不保证永久持有代理实例 |
一致性保障 | 通过这个设计保证跨语言调用的身份和状态一致 |
代理缓存(ProxyCache)设计结构示意,描述了代理对象管理中各种指针和引用的关系,具体解释如下:
ProxyCache 架构示意
组件 | 说明 |
---|---|
Strong pointer | shared_ptr ,拥有对象的所有权,控制对象生命周期 |
Weak pointer | weak_ptr ,非拥有引用,用于检测对象是否仍存活 |
ProxyCache<…> | 缓存管理容器,存储对象的弱引用,避免重复创建代理 |
Non-owning reference (Ref) | 指向原始对象的裸引用(raw pointer 或其他不拥有所有权的引用) |
Proxy | 语言A的代理对象,持有对语言B对象的引用,并实现接口 |
Object | 语言B的真实对象或实现 |
ProxyCache::Handle | 用于访问缓存中的代理和管理生命周期的辅助句柄 |
流程示意
- 原始对象 (Object) 是实际业务实现的实体。
- Proxy 是封装 Object 的代理,暴露给其他语言调用。
- ProxyCache 通过 Ref (非拥有引用)索引,维护对应的代理的弱指针。
- 当需要代理时:
- 查找 ProxyCache,看对应的代理是否还活着(weak_ptr 是否有效)
- 如果存在则返回已有代理(shared_ptr)
- 如果不存在则新建代理,并放入 ProxyCache 缓存
- 代理的生命周期由 shared_ptr 管理,引用计数归零时自动销毁,weak_ptr 失效。
目标
- 唯一性:同一底层对象,任意时间点只有一个代理实例可见
- 生命周期管理:通过 shared_ptr/weak_ptr 保证代理生命周期合理
- 缓存访问:通过非拥有引用(Ref)索引代理,提高查找效率,避免重复构造
如果需要,我可以帮你写一份具体的 ProxyCache 实现示例(C++),说明这个设计模式如何落地。你需要吗?
ProxyCache 设计、生命周期管理、以及Java中各种弱引用的区别,具体解析如下:
1. ProxyCache 是全局单例,但要用 shared_ptr 持有,为什么?
- ProxyCache 需要全局访问和共享
- 用
shared_ptr
管理它的生命周期,方便在不同模块、线程或调用上下文中安全引用 - 防止程序结束时提前销毁,或者多处访问时出现悬挂引用
- 共享所有权(shared ownership)更安全,避免内存管理难题
2. Java中有几种弱引用?
你列出了以下几种指针和引用类型,分别对应跨语言桥接中不同所有权和弱引用的语义:
名称 | Java 表示 | C++ 表示 | Objective-C 表示 | 说明 |
---|---|---|---|---|
UnowningImplPtr | jobject (裸引用) | void* | __unsafe_unretained | 非拥有实现指针,不负责引用计数 |
OwningImplPtr | jobject + 管理 | shared_ptr<void> | __strong id | 拥有实现对象的所有权,负责管理生命周期 |
OwningProxyPtr | shared_ptr<void> | jobject | shared_ptr<void> | 拥有代理对象所有权 |
WeakProxyPtr | weak_ptr<void> 、JavaWeakRef | weak_ptr<void> | __weak id | 弱引用代理对象,用于缓存和生命周期控制 |
UnowningImplPtrHash | JavaIdentityHash | std::hash | UnretainedIdHash | 哈希函数,用于非拥有指针的比较 |
UnowningImplPtrEqual | JavaIdentityEquals | std::equal_to | std::equal_to | 相等比较,用于非拥有指针 |
3. 关键点总结
- Java的弱引用有多种形式,如
JavaWeakRef
和裸引用jobject
,它们的所有权和生命周期不同 - C++对应的智能指针管理模式,分为拥有和非拥有,结合
shared_ptr
和weak_ptr
- Objective-C 使用
__strong
和__weak
修饰符表达所有权和弱引用 - 哈希和比较函数用于支持代理缓存中的查找和比较操作,确保代理对象唯一性和正确访问
总结内容涵盖了Djinni支持Python的整个技术进展和重点:
- Djinni的整体概览
- 如何新增Python语言支持
- 具体实现技巧(如CFFI桥接)
- 代理缓存实现身份语义(保证代理对象唯一)
- 空值(Nullability)处理
- 定制化的Java异常翻译机制
补充了 Nullability 的细节,关键点是:
Intermediate State 空值表示细节
类型 | ObjC 注解 | Java 注解 | C++ 类型 |
---|---|---|---|
Foo | _Nonnull Foo* | @Nonnull Foo | shared_ptr<Foo> |
optional<Foo> | _Nullable Foo* | @CheckForNull Foo | shared_ptr<Foo> |
- ObjC 和 Java 的注解是建议性的,实际代码里会有显式的 null 检查
- C++侧,
shared_ptr<Foo>
表示一个智能指针,但不区分空和非空
当前处理方式
Foo
默认非空optional<Foo>
可以为null- 引入了
nn_shared_ptr<T>
,表示已经显式检查过非空的指针(类似 GSL 的not_null
)
nn_shared_ptr<T>
通过隐式检查确保不为空,提升代码安全。
JNI(Java Native Interface)异常模型,核心内容是:
异常在 JNI 中的处理机制
- Java 线程中有一个“pending”异常状态,表示当前线程是否有挂起的异常
- Java端处理(自动):
- 在 Java 方法返回之前,异常被捕获并设置为 pending 异常
- 在从 JNI 返回到 Java 之后,会检查 pending 异常并抛出
- Djinni生成的 C++ 代码必须对称处理 JNI 异常:
- 在调用 Java 之前,要检查是否有挂起异常,抛出或处理它
- 在从 Java 调用返回后,捕获异常并设置 pending 异常状态,准备传回 Java
简而言之,Djinni生成的桥接代码负责在跨语言调用时:
- 及时捕获异常,防止崩溃或未处理的错误
- 维护 JNI 线程的异常状态,确保异常正确传播到Java层
Djinni默认的Java异常转换机制,核心流程是:
1. Java → C++ 调用返回后
- 调用返回后,Djinni桥接代码会:
- 检查是否有Java层的挂起异常(pending exception)
- 获取并清除该异常
- 抛出一个
djinni::jni_exception
,将Java异常封装起来,抛给C++层
2. C++ → Java 调用返回前
- 在返回Java前,Djinni代码会:
- 捕获 C++ 层的
jni_exception
- 从中提取原始的Java异常对象
- 重新设置该异常为Java线程的挂起异常(pending exception)
- 返回Java层让Java端抛出该异常
- 捕获 C++ 层的
- 如果捕获到的是其他类型的C++异常:
- 会转换为Java的
RuntimeException
并设置为挂起异常,传给Java层
- 会转换为Java的
总结
- Djinni为Java与C++之间的异常传递建立了对称的桥梁
- 保证异常可以跨语言正常传递、捕获和抛出
- 避免未捕获异常导致程序崩溃
Djinni 默认的异常翻译机制虽然免费且简单,但有局限性,比如:
- 自定义异常类型
默认只处理通用的jni_exception
,不支持用户自己定义的复杂异常类或层级 - 跨语言堆栈追踪
默认不会合并或传递C++端的堆栈信息,难以诊断跨语言调用时的错误细节
换句话说,默认的异常翻译适合简单场景,但对于更复杂的业务异常处理需求,或者需要更详细调试信息时,可能不够用。
Djinni 支持 可插拔的异常翻译函数:
- Djinni 默认提供的异常转换函数是用
__attribute__((weak))
标记的 - 这意味着你可以在自己的代码里定义同名函数,链接时会优先使用你自定义的版本
- 这样你就可以覆盖默认的异常转换逻辑,实现自定义的异常映射,比如:
// 伪代码示例
extern "C" void translate_exception_to_java(const std::exception& ex) __attribute__((weak));
void translate_exception_to_java(const std::exception& ex) {if (auto* sec_ex = dynamic_cast<const my_security_exception*>(&ex)) {// 转换成Java层的 SecurityException} else {// 其他异常的默认处理}
}
- 你可以根据自己的 C++ 异常类型,映射到Java中的自定义异常类
- 反向转换也可以自定义,实现完整的双向异常映射
简单说,Djinni给了你一个“钩子”,让你自由定制异常的跨语言传递行为。
Java 异常传递到 C++ 时的处理:
- 当 Java 层抛出异常并传递到 C++ 代码时,Djinni 会调用这个函数:
void jniThrowCppFromJavaException(JNIEnv * env, jthrowable java_exception);
- 这个函数的作用是:
接收一个 Java 异常对象java_exception
,然后把它转换成对应的 C++ 异常并抛出,保证 C++ 代码可以捕获到并处理异常。 - 这是异常跨语言传递的关键接口,必须确保一定会抛出 C++ 异常(不能无视或吞掉异常)。
简单来说,这是Java异常“翻译成”C++异常的桥梁。
这段代码是 jniThrowCppFromJavaException
的一个典型自定义实现示例,主要做了以下工作:
void jniThrowCppFromJavaException(JNIEnv* env, jthrowable java_exception) {const SecurityExceptionClassInfo& ci = djinni::JniClass<SecurityExceptionClassInfo>::get();if (env->IsInstanceOf(java_exception, ci.clazz.get())) {LocalRef<jstring> jmessage{env,(jstring)env->CallObjectMethod(java_exception, ci.getMessage)};throw my_security_exception(jniUTF8FromString(env, jmessage.get()));}throw jni_exception{env, java_exception};
}
``
1. **拿到 `SecurityException` 的 Java 类信息**`const SecurityExceptionClassInfo & ci = djinni::JniClass<SecurityExceptionClassInfo>::get();`这行获取了自定义的 Java `SecurityException` 类的元数据(class reference 和方法 ID)。
2. **判断传入的 `java_exception` 是否是 `SecurityException` 的实例**`env->IsInstanceOf(java_exception, ci.clazz.get())`如果是,就进入特定处理分支。
3. **调用 `getMessage()` 获取异常消息字符串**使用 `env->CallObjectMethod` 调用 Java 异常的 `getMessage` 方法,并封装成 `LocalRef` 来管理本地引用。
4. **抛出对应的 C++ 自定义异常**`throw my_security_exception(jniUTF8FromString(env, jmessage.get()));`把从 Java 取得的异常消息转换成 UTF-8 字符串,构造自定义 C++ 异常 `my_security_exception` 并抛出。
5. **否则抛出默认的 Djinni 异常包装类**如果异常不是 `SecurityException`,用通用的 `jni_exception` 包装后抛出。
总结:这段代码就是一个自定义的 **Java异常 → C++异常** 的转换器,能够识别特定异常类型并转成对应的 C++异常,保证跨语言异常处理的类型和信息一致。
# 这段代码定义了一个结构体 `SecurityExceptionClassInfo`,用来缓存 Java `SecurityException` 类及其相关方法的 JNI 信息,方便后续调用。具体作用是:
```cpp
struct SecurityExceptionClassInfo {const GlobalRef<jclass> clazz = djinni::jniFindClass("java/lang/SecurityException");const jmethodID ctor = djinni::jniGetMethodID(clazz.get(), "<init>", "(Ljava/lang/String;)V");const jmethodID getMessage =djinni::jniGetMethodID(clazz.get(), "getMessage", "()Ljava/lang/String;");
};
clazz
:通过djinni::jniFindClass
找到java/lang/SecurityException
类的全局引用,避免每次都重新查找。ctor
:获取该类的构造函数 ID,签名是(Ljava/lang/String;)V
,表示带一个字符串参数的构造函数。getMessage
:获取getMessage()
方法的 ID,签名是()Ljava/lang/String;
,表示无参数返回字符串。
这样在异常转换时可以方便地调用构造函数或getMessage
方法,且这些 JNI 查找操作只做一次,提升效率。
void jniSetPendingFromCurrent(JNIEnv* env, const char* ctx) noexcept {try {throw;} catch (const my_security_exception& e) {const SecurityExceptionClassInfo& ci = djinni::JniClass<SecurityExceptionClassInfo>::get();LocalRef<jstring> jmessage{env, jniStringFromUTF8(env, e.what())};LocalRef<jthrowable> jexception{env, (jthrowable)env->NewObject(ci.clazz.get(), ci.ctor, jmessage.get())};env->Throw(jexception);return;}catch (const std::exception& e) {jniDefaultSetPendingFromCurrent(env, ctx);}
}
- 当 C++ 抛出的异常传递到 Java 侧时,调用
jniSetPendingFromCurrent
。 - 它在 C++ 的 catch 块里被调用,用来将当前捕获的 C++ 异常转换成一个 Java 异常,并设置为 Java 线程的“pending exception”(待处理异常)。
- 函数签名里
env
是 JNI 环境指针,ctx
是一个上下文字符串(一般是用来描述异常发生场景的,比如函数名或操作描述)。
通常你会这样实现:
- 先捕获具体的 C++ 异常类型(比如自定义的异常)。
- 根据异常内容创建对应的 Java 异常对象(用 JNI 创建)。
- 调用
env->Throw()
或env->ThrowNew()
把 Java 异常设置为挂起状态。 - 函数返回后,Java 代码在下一步调用 JNI 的时候就会收到这个异常。
- 你们通过在 C++ 异常里保存完整的 C++ 栈信息和嵌套的
jni_exception
(表示原始 Java 异常)来实现异常的“链式”传递。 - 在 Java 端,利用 Throwable 的 cause 和可覆写的 stack trace(
StackTraceElement[]
)来构造多层异常堆栈,保证能够显示 C++ 栈、Java 栈,以及它们之间的因果关系。 - 你们定义了一个 Java 端的“构造器”类型,用于动态设置异常信息、因果链和栈信息,一步步构建出完整的异常对象。
- Java → C++ 方向,保留原 Java 异常并捕获 C++ 栈,抛出自定义 C++异常。
- C++ → Java 方向,用构造器创建一串嵌套的 Java异常对象(cause chain),并把它设置为 pending 异常,交给 Java处理。
- 今年还添加了 f32、date 之类新类型,支持外部类型、线程创建服务、异常定制、依赖追踪以及 JetBrains 插件等新功能。
这套方案能大幅提升跨语言异常调试体验,尤其是在复杂的调用链中同时查看 C++和Java的堆栈,定位问题更容易。