ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

cangjie_python_interop:仓颉-Python 互操作库的架构、构建与使用指南

cangjie_python_interop:仓颉-Python 互操作库的架构、构建与使用指南
  • 开发工具

【免费下载链接】cangjie_python_interop

项目地址:https://gitcode.com/Cangjie/cangjie_python_interop
点击查看免费下载

cangjie_python_interop(仓颉-Python 互操作库)为仓颉语言提供与 Python 的跨语言互操作能力,覆盖双向类型转换、导入并调用 Python 模块、执行 Python 表达式与代码,以及 Python 回调仓颉函数四大能力。本文以仓库的 README 为核心骨架,结合src/ffi/py_interop源码、build/build.py构建脚本与tests/测试用例,系统梳理该库的能力边界、目录结构、约束条件与从源码构建的完整流程,帮助你从"看懂文档"走到"能编译、能链接、能调用"。

一、库定位与核心能力

如 README_zh.md 所述,本库用于提供仓颉与 Python 跨语言互操作能力,具体包括四类功能:

  1. Python 数据类型与仓颉类型之间的互相转换——Bool、整型、浮点、字符串、元组、列表、字典、集合、切片等基础类型均有对应的桥接类型(PyBool、PyLong、PyFloat、PyString、PyTuple、PyList、PyDict、PySet、PySlice);
  2. 在仓颉侧导入并使用 Python 模块——标准库、三方模块与用户自定义模块(如numpy)均可导入,并使用模块内的函数、变量、类等所有可见对象;
  3. 执行 Python 表达式与 Python 代码——通过Python.eval()执行单个表达式并返回值,通过Python.exec()执行完整代码块(函数与类定义、模块导入、多行流程控制等);
  4. 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/libspy_interop包 API 参考

从源码结构看,src/ffi/py_interop下各文件与核心类型的对应关系较为直接:

源文件主要承载类型(按源码命名推断)
python.cjPython解释器类、Version、PythonException、LogLevel、PythonLogger(PYLOG)
pyobj.cjPyObj基类:成员访问、函数调用、运算符重载、asPyXxx类型转换
pybase.cjPyBool/PyLong/PyFloat/PyString等基础标量类型
pylist.cj、pytuple.cj、pydict.cj、pyset.cj、pyslice.cj容器类类型PyList/PyTuple/PyDict/PySet/PySlice
pycfunc.cjPyCFunc:Python 回调仓颉函数
pymodule.cjPyModule:Python 模块封装
cjobj.cjCjObj接口及仓颉基础类型的toPyObj()扩展
pythoncapi.cj、pythoncwapper.cjPython 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 构建准备

  1. 准备 Linux 平台的cangjie SDK,并执行对应 SDK 的envsetup脚本,确保 SDK 被正确配置;
  2. 使用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 clean

clean会清除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库的标准流程为:

  1. import py_interop.*导入库,获得解释器类Python、全局日志对象PYLOG与PyObject类型体系;
  2. 调用Python.load()加载解释器资源(进入互操作前的必须步骤),完成后调用Python.unload()释放;
  3. 通过Python.importMod()/Python.eval()/Python.exec()/Python.getVersion()等接口调用 Python 能力;
  4. 用PyObj及其子类创建与操作 Python 对象;
  5. 通过PyCFunc注册仓颉函数,实现 Python 反向调用。

5.1 源码视角下的Python.load()

python.cj 中load(path: String)的实现清晰呈现了加载链路:

  1. 用ReentrantMutex(initLock)保护全局初始化,envInit标志位保证加载/卸载全局只生效一次,与文档"多次调用仅第一次生效"一致;
  2. 校验路径存在后调用loadSo(path)加载动态库,再由readSym()读取符号表;
  3. 调用Py_Initialize()初始化 Python 解释器,随后PyEval_SaveThread()保存并释放 GIL 线程状态;
  4. setPyArgs()将仓颉可执行文件的参数传递给 Python,最后从__builtins__中取出内置函数字典,支撑Python.getBuiltinsAttr()。

Python.isLoad()(python.cj)在加锁状态下读取envInit;unload()则执行CJ_UnbindOSThread()解绑系统线程并清理资源,未先load时unload仅打印 warn 并返回。

5.2 Python 动态库的三步查找策略

libpython的搜索顺序由无参load()内部调用getPythonPath()完成(python.cj 的注释即完整描述了三步策略):

  1. 环境变量PYTHON_DYNLIB:

    export PYTHON_DYNLIB=".../libpython3.9.so"
  2. 可执行文件依赖:执行ldd $(which python3),从python3的动态库依赖中提取libpython路径(源码中对应常量cmdExeDep,见 python.cj)。注意:非动态库依赖的 Python(源码编译未加--enable-shared)无法被此步找到;

  3. 系统默认路径:依次在/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.loadstatic func load(loglevel!: LogLevel = LogLevel.WARN)/load(path: String, loglevel!: LogLevel = LogLevel.WARN)加载解释器;支持无参自动查找或指定动态库绝对路径;失败抛PythonException
Python.unloadstatic func unload(): Unit卸载解释器资源,须与load一一对应
Python.isLoadstatic func isLoad(): Bool判断 Python 库是否已加载
Python.getVersionstatic func getVersion(): Version获取当前 Python 版本(Version不可变结构,提供getMajor/Minor/Micro、getVersion()三元组与toString),未load时返回0.0.0
Python.importModstatic func importMod(module: String): PyModule导入标准库/三方/自定义模块;模块不存在时抛仓颉异常,异常信息含ModuleNotFoundError详情
Python.evalstatic func eval(cmd: String, module!: String = "__main__"): PyObj执行单个表达式并返回值;仅支持表达式(赋值等语句会报SyntaxError),cmd长度需在 (0, 1000]
Python.execstatic func exec(code: String): Unit执行完整代码块(定义、导入、多行流程控制)
Python.getBuiltinsAttrstatic 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 TypeBridge TypePython Type
BoolPyBoolbool
UInt8/Int8/Int16/UInt16/Int32/UInt32/Int64/UInt64PyLongint
Float32/Float64PyFloatfloat
Rune/StringPyStringstr
-PyTupletuple
Array< T >PyList< T >list
HashMap< K, V >PyDict< K, V >dict
HashSet< T >PySet< T >set
Range< T >PySlice< T >slice
(Array< PyObj >) -> PyObjPyCFuncbuiltin_function_or_method

Python 类型到仓颉类型映射:

Python TypeBridge TypeCangjie Type
boolPyBoolBool
intPyLongInt64/UInt64
floatPyFloatFloat64
strPyStringString
tuplePyTupleArray< PyObj >
listPyList< PyObj >Array< PyObj >
dictPyDict< PyObj, PyObj >HashMap< PyObj, PyObj >
setPySet< PyObj >HashSet< PyObj >
slicePySlice< 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

项目地址:https://gitcode.com/Cangjie/cangjie_python_interop
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进