- 开发工具
【免费下载链接】cangjie_python_interop
cangjie_python_interop(仓颉-Python 互操作库)为仓颉语言提供与 Python 的跨语言互操作能力,覆盖双向类型转换、导入并调用 Python 模块、执行 Python 表达式与代码,以及 Python 回调仓颉函数四大能力。本文以仓库的 README 为核心骨架,结合src/ffi/py_interop源码、build/build.py构建脚本与tests/测试用例,系统梳理该库的能力边界、目录结构、约束条件与从源码构建的完整流程,帮助你从"看懂文档"走到"能编译、能链接、能调用"。
一、库定位与核心能力
如 README_zh.md 所述,本库用于提供仓颉与 Python 跨语言互操作能力,具体包括四类功能:
- Python 数据类型与仓颉类型之间的互相转换——
Bool、整型、浮点、字符串、元组、列表、字典、集合、切片等基础类型均有对应的桥接类型(PyBool、PyLong、PyFloat、PyString、PyTuple、PyList、PyDict、PySet、PySlice); - 在仓颉侧导入并使用 Python 模块——标准库、三方模块与用户自定义模块(如
numpy)均可导入,并使用模块内的函数、变量、类等所有可见对象; - 执行 Python 表达式与 Python 代码——通过
Python.eval()执行单个表达式并返回值,通过Python.exec()执行完整代码块(函数与类定义、模块导入、多行流程控制等); - Python 回调仓颉函数——构造
PyCFunc对象后直接传递给 Python 侧调用,支持 Python 侧各类数据作为回调入参。
更完整的接口说明参见 用户指南 与 开发者指南,API 参考文档位于 docs/libs/py_interop/py_interop_package_overview.md。
二、仓库目录结构
README 给出的目录结构如下:
cangjie_interop/ ├── build # 构建脚本 ├── docs # 介绍文档,包括用户指南、开发者指南、api参考 ├── src │ ├── ffi │ └── py_interop # 仓颉-Python 互操作库源码 └── tests # 测试用例结合仓库实际内容,各目录的职责如下:
| 路径 | 内容与职责 |
|---|---|
| build/build.py | 构建入口脚本,封装cjpm命令,提供build/install/clean三个子命令 |
| src/ffi/py_interop | 互操作库全部源码,共 14 个.cj文件、约 4000 行 |
| tests/py_interop | 按功能分类的测试用例(cfunc、eval、exec、import、object、slice 等 12 个目录) |
| docs/user-guide.md、docs/developer-guide.md | 使用指南与开发者指南 |
| docs/libs | py_interop包 API 参考 |
从源码结构看,src/ffi/py_interop下各文件与核心类型的对应关系较为直接:
| 源文件 | 主要承载类型(按源码命名推断) |
|---|---|
| python.cj | Python解释器类、Version、PythonException、LogLevel、PythonLogger(PYLOG) |
| pyobj.cj | PyObj基类:成员访问、函数调用、运算符重载、asPyXxx类型转换 |
| pybase.cj | PyBool/PyLong/PyFloat/PyString等基础标量类型 |
| pylist.cj、pytuple.cj、pydict.cj、pyset.cj、pyslice.cj | 容器类类型PyList/PyTuple/PyDict/PySet/PySlice |
| pycfunc.cj | PyCFunc:Python 回调仓颉函数 |
| pymodule.cj | PyModule:Python 模块封装 |
| cjobj.cj | CjObj接口及仓颉基础类型的toPyObj()扩展 |
| pythoncapi.cj、pythoncwapper.cj | Python C API 的foreign func声明与 C 侧包装 |
| slab.cj | 内部内存分配支撑 |
三、约束条件:平台与后端限制
README_zh.md 明确了当前的约束:
目前仅支持在 Linux 平台编译和使用,并且仅支持仓颉编译器的 cjnative 后端。
这一点在仓库中可以得到多处印证:
- 构建脚本 build/build.py 中有硬性校验:非 Linux 或
--target不为native时直接报错py_interop compile is only supported for native Linux,且SUPPORTED_TARGET = ["native"]只允许本机平台; - 包配置文件 src/ffi/py_interop/cjpm.toml 中,Linux 目标(
x86_64-unknown-linux-gnu)通过--cfg "py_interop_plat = Linux"注入平台宏,源码中用@When[py_interop_plat == "ohos"]等条件编译区分平台行为;同时该文件还包含aarch64-linux-ohos、x86_64-linux-ohos的交叉编译配置,与 用户指南 中"支持在 Linux 和 OpenHarmony 平台上仓颉调用 Python"的表述相互对应。
因此,在使用前需要确认两点:运行环境是 Linux,且仓颉工程采用cjnative 后端。
四、从源码编译构建
开发者指南 给出的安装与使用流程如下,这里结合 build/build.py 的实现补充细节。
4.1 构建准备
- 准备 Linux 平台的
cangjie SDK,并执行对应 SDK 的envsetup脚本,确保 SDK 被正确配置; - 使用
python构建脚本方式编译,需安装python3。
4.2 构建步骤
cd build python3 build.py build -t release-t, --build-type指定构建产物版本,可选值为debug/release,默认release。从 build/build.py 的源码看,该命令实际是在src/ffi/py_interop目录下调用cjpm build(debug对应cjpm build --debug,release对应cjpm build --release),因此构建依赖 SDK 自带的cjpm工具链。src/ffi/py_interop/cjpm.toml 声明了包信息:包名py_interop、版本1.0.0、cjc-version = "0.53.21"、output-type = "dynamic"(即产出动态库),target-dir指向仓库根目录下的target。
4.3 安装到指定目录
# 默认安装到 target 目录 python3 build.py install # 通过 --prefix 指定安装目录 python3 build.py install --prefix ./output从源码实现(build/build.py)可以看到,install会检查target/release/py_interop是否存在,若不存在则提示先执行build;存在时把两个编译产物拷贝到目标目录:
output/ |-- py_interop.cjo # cjo 文件(仓颉包元数据) |-- libpy_interop.so # 共享库文件4.4 清理与更多构建选项
python3 build.py cleanclean会清除target目录下的全部构建产物。build子命令还提供:
--target TARGET:指定编译目标产物的运行平台,默认native(本机平台),当前仅支持 Linux;-h, --help:打印build功能的帮助信息;build.py顶层同样支持-h, --help。
4.5 编译产物如何被使用
编译并安装后,用户工程在调用cjc时需要将py_interop目录同时加入链接路径与导入路径。仓库自身的测试配置 tests/test.cfg 给出了标准用法:
pyffi-link = -l py_interop -L $CANGJIE_STDX_PATH/../../../py_interop --import-path $CANGJIE_STDX_PATH/../../../py_interop即编译命令形如cjc -l py_interop -L /path/to/py_interop --import-path /path/to/py_interop ./main.cj -o ./main。此外,用户指南 特别提示:执行 Python 互操作相关代码前,建议将仓颉默认栈大小配置为至少 1MB(export cjStackSize=1MB),因为互操作过程会大量调用 Python 库的 native 代码,超出默认 64KB 栈保护可能触发不可预期的结果。
五、快速上手:加载解释器与最小互操作示例
README 指引读者参阅 docs 目录下的指南,这里给出最小可运行示例(来自 用户指南):导入 Python 标准库sys模块并访问其version属性。
import py_interop.* main(): Int64 { Python.load() var sys: PyObj = Python.importMod("sys") println(sys["version"]) Python.unload() return 0 }执行结果(与指南一致,具体版本号取决于系统安装的 Python):
3.10.12 (main, May 27 2025, 17:12:29) [GCC 11.4.0]使用py_interop库的标准流程为:
import py_interop.*导入库,获得解释器类Python、全局日志对象PYLOG与PyObject类型体系;- 调用
Python.load()加载解释器资源(进入互操作前的必须步骤),完成后调用Python.unload()释放; - 通过
Python.importMod()/Python.eval()/Python.exec()/Python.getVersion()等接口调用 Python 能力; - 用
PyObj及其子类创建与操作 Python 对象; - 通过
PyCFunc注册仓颉函数,实现 Python 反向调用。
5.1 源码视角下的Python.load()
python.cj 中load(path: String)的实现清晰呈现了加载链路:
- 用
ReentrantMutex(initLock)保护全局初始化,envInit标志位保证加载/卸载全局只生效一次,与文档"多次调用仅第一次生效"一致; - 校验路径存在后调用
loadSo(path)加载动态库,再由readSym()读取符号表; - 调用
Py_Initialize()初始化 Python 解释器,随后PyEval_SaveThread()保存并释放 GIL 线程状态; setPyArgs()将仓颉可执行文件的参数传递给 Python,最后从__builtins__中取出内置函数字典,支撑Python.getBuiltinsAttr()。
Python.isLoad()(python.cj)在加锁状态下读取envInit;unload()则执行CJ_UnbindOSThread()解绑系统线程并清理资源,未先load时unload仅打印 warn 并返回。
5.2 Python 动态库的三步查找策略
libpython的搜索顺序由无参load()内部调用getPythonPath()完成(python.cj 的注释即完整描述了三步策略):
环境变量
PYTHON_DYNLIB:export PYTHON_DYNLIB=".../libpython3.9.so"可执行文件依赖:执行
ldd $(which python3),从python3的动态库依赖中提取libpython路径(源码中对应常量cmdExeDep,见 python.cj)。注意:非动态库依赖的 Python(源码编译未加--enable-shared)无法被此步找到;系统默认路径:依次在
/lib、/usr/lib、/usr/local/lib中查找,文件名必须满足libpythonX.Y.so命名,支持d.so、m.so、dm.so、.so等后缀。
如果三步都失败,会抛出PythonException(如Can't get path from system lib path, load exit.)。排查时可开启INFO级日志(Python.load(loglevel: LogLevel.INFO))观察完整搜索过程。关于动态库获取,用户指南 推荐从 Python 源码编译:
# 在Python源码路径下: ./configure --enable-shared --with-system-ffi --prefix=/usr make make install用户指南建议 Python 3.9.2,并声明支持读取 Python 3.0 以上版本;当前源码中还定义了解释器版本上限常量TOP_MOJOR_VERSION = 3、TOP_MINOR_VERSION = 12(python.cj),可用于校验所链接动态库的版本兼容性。
5.3 核心接口速览
| 接口 | 签名(节选) | 说明 |
|---|---|---|
Python.load | static func load(loglevel!: LogLevel = LogLevel.WARN)/load(path: String, loglevel!: LogLevel = LogLevel.WARN) | 加载解释器;支持无参自动查找或指定动态库绝对路径;失败抛PythonException |
Python.unload | static func unload(): Unit | 卸载解释器资源,须与load一一对应 |
Python.isLoad | static func isLoad(): Bool | 判断 Python 库是否已加载 |
Python.getVersion | static func getVersion(): Version | 获取当前 Python 版本(Version不可变结构,提供getMajor/Minor/Micro、getVersion()三元组与toString),未load时返回0.0.0 |
Python.importMod | static func importMod(module: String): PyModule | 导入标准库/三方/自定义模块;模块不存在时抛仓颉异常,异常信息含ModuleNotFoundError详情 |
Python.eval | static func eval(cmd: String, module!: String = "__main__"): PyObj | 执行单个表达式并返回值;仅支持表达式(赋值等语句会报SyntaxError),cmd长度需在 (0, 1000] |
Python.exec | static func exec(code: String): Unit | 执行完整代码块(定义、导入、多行流程控制) |
Python.getBuiltinsAttr | static func getBuiltinsAttr(attr: String): Option<PyObj> | 按名称获取 Python 内置函数,未找到返回None |
importMod的模块路径规则(来自 用户指南):内置模块(如sys、time)无需路径配置;标准库模块(如os、json)依赖 Python 安装路径,Linux 默认/usr通常无需设置,OpenHarmony 需设置PYTHONHOME;三方与自定义模块需通过PYTHONPATH环境变量或运行时向sys.path插入路径来配置搜索路径。
六、Python 对象体系与类型映射
互操作基于 Python C API 开发,所有跨语言数据交换统一通过PyObject结构体指针完成。仓颉侧将其封装为基类PyObj,并按 Python 数据类型派生子类。用户指南 给出的双向类型映射表是理解整个对象体系的总纲:
仓颉类型到 Python 类型映射:
| Cangjie Type | Bridge Type | Python Type |
|---|---|---|
| Bool | PyBool | bool |
| UInt8/Int8/Int16/UInt16/Int32/UInt32/Int64/UInt64 | PyLong | int |
| Float32/Float64 | PyFloat | float |
| Rune/String | PyString | str |
| - | PyTuple | tuple |
| Array< T > | PyList< T > | list |
| HashMap< K, V > | PyDict< K, V > | dict |
| HashSet< T > | PySet< T > | set |
| Range< T > | PySlice< T > | slice |
| (Array< PyObj >) -> PyObj | PyCFunc | builtin_function_or_method |
Python 类型到仓颉类型映射:
| Python Type | Bridge Type | Cangjie Type |
|---|---|---|
| bool | PyBool | Bool |
| int | PyLong | Int64/UInt64 |
| float | PyFloat | Float64 |
| str | PyString | String |
| tuple | PyTuple | Array< PyObj > |
| list | PyList< PyObj > | Array< PyObj > |
| dict | PyDict< PyObj, PyObj > | HashMap< PyObj, PyObj > |
| set | PySet< PyObj > | HashSet< PyObj > |
| slice | PySlice< Int64 > | Range< Int64 > |
几条关键约定:
- 仓颉侧泛型容器(
PyList<T>、PyDict<K,V>、PySet<T>)要求元素类型约束为PyFFIType接口的子类,包括PyObj与实现CjObj的仓颉基础类型; - 仓颉
Array<PyObj>数组可直接构造PyTuple; - Python 侧的自定义类实例、函数、模块等一切可调用/可命名对象,均以
PyObj形式传入仓颉侧。
PyObj基类(pyobj.cj)提供了通用操作:isAvailable()判断封装指针是否可用;[]重载实现成员读写(写入不可用值会删除对应键);()重载实现无参/多参/kargs命名参数调用(参数支持CjObj与PyObj,但不允许混用);+ - * / ** %运算符重载遵循 Python 语义,非法组合会打印 warn 并返回不可用对象;asPyLong/asPyFloat/asPyString/asPyBool/asPyTuple/asPyDict/asPyList/asPySet/asPySlice提供类型匹配转换,类型不符时抛异常。
七、Python 回调仓颉函数(PyCFunc)
PyCFunc是双向互操作的另一半:它允许把签名为(Array<PyObj>) -> PyObj的仓颉函数注册给 Python 侧(pycfunc.cj),回调参数逐一按PyObj解析,返回值需构造一个PyObj。
func foo(args: Array<PyObj>): PyObj { let obj0 = args[0].asPyLong() println("${obj0.toCjObj()}") PyList<PyObj>([PyLong(12345), PyString("ret")]) } main() { Python.load() let pyFunc = PyCFunc(foo) // 注册回调 pyFunc(42, "hello", [4, 5, 6]) // 仓颉侧也可直接调用 let mm = Python.importMod("main") mm"testFoo" // 传递给 Python 侧调用 Python.unload() 0 }在完整示例中(用户指南),Python 侧一次性传入int/str/tuple/dict/list/bool/float/set/slice/自定义类实例/函数对象十一种参数,仓颉侧全部正确解析,并返回[12345, 'ret', [1, 2, 3]]。CFunc函数入参和返回值支持的类型包括:Bool/Int8/UInt8/Int16/UInt16/Int32/UInt32/Int64/UInt64/Float32/Float64/CPointer<Unit>/CString/Unit。
八、并发、异常与全局资源
并发模型。程序运行时全局仅加载一个 Python 解释器,通过全局解释器锁(GIL)保证并发访问安全。互操作接口支持系统线程间并发,但不支持仓颉协程间直接并发:在spawn出的协程中调用互操作接口前,需通过CJ_BindOSThread()将协程绑定到系统线程(该foreign func的声明即位于 python.cj,且load()内部也会为主线程自动绑定):
foreign func CJ_BindOSThread(): Int32 foreign func CJ_UnbindOSThread(): Int32 main() { Python.load() spawn { unsafe {CJ_BindOSThread()} var sys = Python.importMod("sys") sys["version"] unsafe {CJ_UnbindOSThread()} } Python.unload() 0 }未绑定系统线程的spawn在调用期间可能因线程切换抛出异常;同时拉起超过 8 个绑定系统线程的spawn可能出现死锁(用户指南将其标注为仓颉运行时已知问题)。
异常体系。库内所有异常统一为PythonException(python.cj),可通过try-catch捕获;未捕获时打印堆栈并以返回值 1 退出。回调场景下,仓颉回调函数中抛出的PythonException堆栈会与 Python 侧堆栈一同打印,便于跨语言定位。
全局日志。全局日志对象PYLOG(PythonLogger实例,定义于 python.cj)默认打印等级为WARN,提供debug/info/warn/error与log(level, msg)接口,前缀区分级别(如WARN: log warn);不支持日志转储文件。load的loglevel参数不配置时会将PYLOG重置为WARN。
九、测试体系
tests/目录基于 cangjie_test_framework 测试框架组织(该框架为独立仓库,tests/test.sh 头部注释给出了配置依赖的方法)。测试用例按功能划分为 12 个子目录:
tests/py_interop/ ├── cfunc/ # PyCFunc 回调(含 refcount、spawn 场景) ├── compare/ # ==/!= 与 hashCode ├── eval/ # eval 表达式执行(含 evalTest.py) ├── exec/ # exec 代码块执行 ├── global/ # load 生命周期、日志、解释器异常、spawn 并发 ├── import/ # 模块导入(importTest.py、class/module 场景) ├── index/ # [] 下标操作 ├── iterator/ # PyObjIterator 迭代 ├── kargs_call/ # 命名参数调用 ├── object/ # 各基础/容器类型的构造与映射(python_dict/list/set/tuple 等) ├── object_operation/ # 运算符重载 └── slice/ # PySlice 切片运行入口 tests/test.sh 调用测试框架主程序,关键参数为--fail_exit --keep_temp --retry=3 --timeout=600 -j16;测试清单 tests/testlist 以[ALL-TEST-CASE]全量选择py_interop目录;tests/test.cfg 声明测试条件为LLVM/x86/Linux/Unix,并在[env]中将当前目录前置到LD_LIBRARY_PATH。运行前需按test.sh注释要求:先编译py_interop,把py_interop.cjo与libpy_interop.so拷贝到$CANGJIE_STDX_PATH/../../../py_interop目录,并 export 到LD_LIBRARY_PATH。
十、开源协议与参与贡献
本项目基于 Apache-2.0 with Runtime Library Exception 与 Attribution 4.0 International 双重许可发布(源码文件头部同时声明了两者的版权与授权信息)。项目欢迎提交 Issue 与任何形式的参与贡献;构建与测试工具链则依赖同体系的 cangjie_test_framework 开源仓库(见 tests/test.sh 中的说明)。
十一、小结
cangjie_python_interop 以"一个包(py_interop)、一个解释器门面(Python类)、一套对象桥接体系(PyObj及子类)"为核心组织:Python.load/unload管理全局唯一的解释器生命周期,importMod/eval/exec打通仓颉到 Python 的调用通道,PyObj对象体系与CjObj接口完成双向类型转换,PyCFunc补全 Python 反向回调仓颉的闭环。使用上需要牢记三件事:仅限 Linux + cjnative 后端;load与unload严格一一对应且全局只生效一次;协程并发调用前必须绑定系统线程。掌握以上内容后,即可按本文第四节的流程完成从源码构建,到cjc -l py_interop -L ... --import-path ...链接、再到真实调用 Python 生态的完整链路。
- 开发工具
【免费下载链接】cangjie_python_interop
相关推荐
cangjie_python_interop 仓颉-Python 互操作库:跨语言桥接能力与源码级实战指南
cangjie_python_interop 仓颉 Python 互操作库:跨语言桥接能力与源码级实战指南 本文围绕 cangjie_python_intero
开发工具仓颉-C互操作(FFI)实战:如何在仓颉中调用C库的完整指南
仓颉 C互操作(FFI)实战:如何在仓颉中调用C库的完整指南 想让仓颉程序直接调用成熟的 C 库吗?仓颉编程语言通过 FFI(Foreign Function
文档编程语言大麦自动抢票:双端 Web + 移动端自动化抢票完整指南,3步跑通
大麦自动抢票:双端 Web + 移动端自动化抢票完整指南,3步跑通 ticket purchase 是一个用 Python 写的大麦自动抢票工具:网页端用 Se
GUI 自动化RPA
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考