PuerTS 3.0 实战:在 C# 中调用 Python —— Delegate 桥接、参数传递、返回值与异常处理全解析
【免费下载链接】puertsPUER(普洱) Typescript. Let's write your game in UE or Unity with TypeScript.项目地址: https://gitcode.com/GitHub_Trending/pu/puerts
本篇技术指南以 PuerTS 3.0 的 Python 后端(
BackendPython)为对象,系统讲解如何在 C# 侧调用 Python 函数:从最核心的"将 Python 函数转换为 C# delegate"能力出发,覆盖参数传递、返回值获取、异常捕获、环境生命周期管理,并最终落地为一个"在 Python 中实现 MonoBehaviour 生命周期回调"的完整实战方案。读完本文,你将掌握 PuerTS 中 C# ↔ Python 双向互调的标准写法,并能对照 JavaScript 与 Lua 的差异快速切换语言。
PuerTS 3.0 同时支持 C# 调用 Javascript 和 Lua,三者共用统一的ScriptEnv+Backend架构,仅Backend类型不同(详见 三语言对比速查表)。Python 侧的环境创建方式是:
var env = new Puerts.ScriptEnv(new Puerts.BackendPython());ScriptEnv是所有脚本后端的统一宿主(实现见 unity/upms/core/Runtime/Src/PInvoke/ScriptEnv.cs),它持有Backend创建的环境引用与底层 papi API;而BackendPython(实现见 unity/upms/python/Runtime/Src/Backends/BackendPython.cs)负责创建 Python 解释器环境、获取 Python 的 FFI API,并自带默认加载器。环境使用完毕后务必调用env.Dispose()释放。
一、通过 Delegate 调用 Python 函数
PuerTS 提供了一个关键能力:将 Python 函数转换为 C# 的 delegate。依靠这个能力,你就可以在 C# 侧像调用普通 C# 方法一样调用 Python 函数。
1.1 把 Python 函数赋给 C# 的 delegate 属性
下面的例子定义了一个TestCallbackdelegate 和一个持有该 delegate 属性的TestClass,然后在 Python 中创建TestClass实例、把 Python 函数赋给obj.Callback,最后从 C# 侧触发TriggerCallback():
public delegate void TestCallback(string msg); public class TestClass { public TestCallback Callback; public void TriggerCallback() { if (Callback != null) { Callback("hello_from_csharp"); } } } void Start() { var env = new Puerts.ScriptEnv(new Puerts.BackendPython()); env.Eval(@" exec(''' import Puerts.UnitTest.TestClass as TestClass obj = TestClass() def callback(msg): global info info = msg # Assign a Python function to the C# delegate property obj.Callback = callback # Trigger the callback from C# side obj.TriggerCallback() ''') "); // info is now 'hello_from_csharp' env.Dispose(); }⚠️注意:Python 中多行代码需要使用
exec('''...''')包裹。单行表达式可以直接用Eval执行。
这段代码的机制可以从测试用例中得到印证:在 unity/test/Src/Cases/Python/CrossLang/DelegateTest.cs 中,DelegateBase测试同样使用def callback(msg)定义函数、deleteobj.Callback = callback赋值,然后通过pythonEnv.Eval<string>("info")读取 Python 全局变量info来断言回调确实被 C# 侧触发:
pythonEnv.Eval(@" exec(''' deleteobj = puerts.load_type('Puerts.UnitTest.DelegateTestClass')() def callback(msg): global info info = msg deleteobj.Callback = callback deleteobj.CSMessage() ''') "); string info = pythonEnv.Eval<string>("info"); Assert.AreEqual("cs_msg", info);注意其中两种访问 C# 类型的方式:文档示例用import Puerts.UnitTest.TestClass as TestClass,测试用例用puerts.load_type('...')——二者等价,前者适合常规场景,后者适合动态加载或类型名含特殊字符(如嵌套类型的+)的场景,详见 在 Python 中调用 C#。
1.2 在 Python 侧主动调用 delegate
反过来,你也可以在 Python 侧主动调用 delegate 的Invoke方法,把参数从 Python 传回 C#:
# Directly invoke the delegate from Python obj.Callback.Invoke('hello_from_python')DelegateTest.cs中的DelegateBase测试对这一步也有覆盖:先由 C# 触发(断言info == "cs_msg"),再调用deleteobj.Callback.Invoke('js_msg'),随后断言info == "js_msg",验证了"Python 侧 Invoke delegate"这条反向通路确实可用。
二、从 C# 往 Python 传参
把 Python 函数转换成 delegate 时,可以将其转换成带参数的 delegate,这样就可以把 C# 变量传递给 Python。传参时,类型转换的规则和把变量从 C# 返回到 Python 是一致的。
2.1 使用lambda表达式创建匿名函数
Python 支持使用lambda表达式来创建简单的匿名函数:
void Start() { var env = new Puerts.ScriptEnv(new Puerts.BackendPython()); // Get a Python lambda as a C# delegate System.Action<int> LogInt = env.Eval<System.Action<int>>("lambda a: print(a)"); LogInt(3); // Output: 3 env.Dispose(); }2.2 使用def定义命名函数
对于更复杂的逻辑,使用def定义函数,然后通过Eval获取:
void Start() { var env = new Puerts.ScriptEnv(new Puerts.BackendPython()); // Define a function with def, then retrieve it env.Eval(@" exec(''' def log_int(a): print(a) ''') "); System.Action<int> LogInt = env.Eval<System.Action<int>>("log_int"); LogInt(3); // Output: 3 env.Dispose(); }2.3 可选参数与多签名 delegate
Python 函数还支持可选参数,转换为不同签名的 delegate 后都可以正常工作:
void Start() { var env = new Puerts.ScriptEnv(new Puerts.BackendPython()); env.Eval(@" exec(''' def flexible_func(a, b=0): if b == 0: return str(a) else: return str(a) + str(b) ''') "); // Cast as Action<int> — only pass the first argument var cb1 = env.Eval<Action<int>>("flexible_func"); cb1(1); // Uses default b=0 // Cast as Action<string, long> — pass both arguments var cb2 = env.Eval<Action<string, long>>("flexible_func"); cb2("hello", 999); // Output: hello999 env.Dispose(); }需要注意的是,如果你生成的 delegate 带有值类型参数,需要添加UsingAction或者UsingFunc声明。具体请参见 FAQ:值类型参数(如
int、long、struct)意味着底层需要反射生成对应的 delegate bridge,在 IL2CPP 等裁剪环境下会失败,必须先通过JsEnv.UsingAction<T1, T2...>()(无返回值)或JsEnv.UsingFunc<T1, T2..., TResult>()(有返回值)显式声明。此外当前版本 delegate 参数数量最多支持 4 个,且暂不支持含ref、out修饰的参数。
三、从 C# 调用 Python 并获得返回值
与上一部分类似,只需要将 Action delegate 变成Func delegate就可以了。
3.1 使用lambda表达式(适合简单的单行逻辑)
void Start() { var env = new Puerts.ScriptEnv(new Puerts.BackendPython()); // Python lambda can directly return a value System.Func<int, int> Add3 = env.Eval<System.Func<int, int>>("lambda a: 3 + a"); System.Console.WriteLine(Add3(1)); // Output: 4 env.Dispose(); }3.2 使用def定义函数(适合复杂逻辑)
void Start() { var env = new Puerts.ScriptEnv(new Puerts.BackendPython()); env.Eval(@" exec(''' def add3(a): return 3 + a ''') "); System.Func<int, int> Add3 = env.Eval<System.Func<int, int>>("add3"); System.Console.WriteLine(Add3(1)); // Output: 4 env.Dispose(); }3.3 直接使用Eval<T>获取简单返回值
如果你只是需要某个 Python 表达式的计算结果,可以直接用泛型Eval<T>:
void Start() { var env = new Puerts.ScriptEnv(new Puerts.BackendPython()); // Directly evaluate a Python expression and get the return value int result = env.Eval<int>("1 + 2"); System.Console.WriteLine(result); // Output: 3 string str = env.Eval<string>("'hello python'"); System.Console.WriteLine(str); // Output: hello python // Convert non-string types with Python builtins var ret = env.Eval<string>("str(9999)"); System.Console.WriteLine(ret); // Output: 9999 env.Dispose(); }⚠️与 Lua 的差异:Python 的
lambda表达式会自动返回结果(类似 JS),无需显式return。但def定义的函数中必须使用return语句返回值,否则返回None。相比之下 Lua 的Eval无论哪种情况都必须显式return(参见 在 C# 中调用 Lua),而 JS 的Eval会返回表达式最后一个值。
四、Python 中的错误处理
当 Python 代码中使用raise抛出异常时,C# 侧可以通过try-catch捕获,异常消息会被完整传递到 C# 侧:
void Start() { var env = new Puerts.ScriptEnv(new Puerts.BackendPython()); // Python raise will be caught as a C# exception try { env.Eval(@" exec(''' raise Exception('something went wrong') ''') "); } catch (Exception e) { Debug.Log(e.Message); // Contains: something went wrong } // SyntaxError is also catchable try { env.Eval(@" exec(''' def test(): return 1 + ''') "); } catch (Exception e) { Debug.Log(e.Message); // Contains: SyntaxError } // RuntimeError (e.g. KeyError) is catchable too try { env.Eval(@" exec(''' obj = {} obj['nonexistent']() ''') "); } catch (Exception e) { Debug.Log(e.Message); // Contains: KeyError } env.Dispose(); }从源码结构看,这套异常桥接机制在原生层完成:PuerTS 的 Python 后端(unity/native/papi-python/source/PapiPythonImpl.cpp)通过PyErr_GetRaisedException()/PyErr_Fetch()捕获 Python 侧的异常,再用traceback.format_exception格式化出完整异常信息,最终通过setException传递到 C# 侧抛出。因此raise、SyntaxError、KeyError、ModuleNotFoundError等各种 Python 异常都能统一变成 C# 的Exception。
单元测试 unity/test/Src/Cases/Python/ExceptionTest.cs 对错误处理有系统覆盖,包括:
ThrowString/ThrowNone/ThrowInFunction:验证raise Exception(...)及转换为 delegate 的 Python 函数内部抛错都能被Assert.Catch捕获;FunctionNotExistsException/InvalidArgumentsException:调用不存在的方法、传错参数类型会抛异常;- 不同 Python 版本对部分类型转换的行为存在差异(如 dict 转 long),测试注释中亦有说明。
更完整的错误类型清单(含
ModuleNotFoundError等)可参见 Python 入门教程。
五、环境销毁与 Delegate 生命周期
当 Python 环境(ScriptEnv)被Dispose()后,之前转换的 delegate 将不再可用。调用已销毁环境的 delegate 会抛出异常,请务必注意管理好生命周期:
void Start() { var env = new Puerts.ScriptEnv(new Puerts.BackendPython()); System.Action callback = env.Eval<System.Action>("lambda: print('hello')"); callback(); // OK — Output: hello env.Dispose(); // ❌ This will throw an exception! // callback(); }ScriptEnv实现了IDisposable接口(见 unity/upms/core/Runtime/Src/PInvoke/ScriptEnv.cs),Dispose()会销毁后端创建的解释器环境引用(BackendPython.DestroyEnvRef)。delegate 底层绑定的是该环境的对象与函数,环境销毁后这些对象随之失效,因此必须保证 delegate 的生命周期不超出ScriptEnv的生命周期。推荐的做法是:在脚本函数转换出的 delegate 不再需要、或环境即将销毁前,主动将其置空,避免悬空引用导致运行时异常。
六、综合实战:在 Python 中实现 MonoBehaviour
综合上面所有能力,我们可以在 Python 里实现 MonoBehaviour 的生命周期回调——把Start、Update、OnDestroy等 C# 生命周期事件委托给 Python 函数处理:
using System; using Puerts; using UnityEngine; public class PythonBehaviour : MonoBehaviour { public Action PythonStart; public Action PythonUpdate; public Action PythonOnDestroy; static ScriptEnv pythonEnv; void Awake() { if (pythonEnv == null) pythonEnv = new ScriptEnv(new BackendPython()); pythonEnv.Eval(@" exec(''' import UnityEngine.MonoBehaviour as MonoBehaviour def init_behaviour(bindTo): def on_update(): print(""update..."") def on_destroy(): print(""onDestroy..."") bindTo.PythonUpdate = on_update bindTo.PythonOnDestroy = on_destroy ''') "); var init = pythonEnv.Eval<Action<MonoBehaviour>>("init_behaviour"); if (init != null) init(this); } void Start() { if (PythonStart != null) PythonStart(); } void Update() { if (PythonUpdate != null) PythonUpdate(); } void OnDestroy() { if (PythonOnDestroy != null) PythonOnDestroy(); PythonStart = null; PythonUpdate = null; PythonOnDestroy = null; } }这个示例串联了本文的全部知识点:
- 环境复用:用
static字段持有ScriptEnv,多个组件实例共享同一个 Python 环境; - 多行代码:所有 Python 逻辑均用
exec('''...''')包裹; - 类型导入:通过
import UnityEngine.MonoBehaviour as MonoBehaviour访问 C# 类型(本示例实际未直接使用,但保留了导入写法); - delegate 赋值:把 Python 的
def函数赋给 C# 的Action属性bindTo.PythonUpdate/bindTo.PythonOnDestroy; - 获取并调用:
env.Eval<Action<MonoBehaviour>>("init_behaviour")取回 Python 函数并传入this完成绑定; - 生命周期管理:
OnDestroy中把三个 delegate 全部置空,与第五节的生命周期注意事项相呼应。
⚠️ 注意 Python 与其他语言的关键差异:
- Python 多行代码需要
exec('''...''')包裹- Python 使用
def定义函数,无需end或花括号- Python 使用
import语法访问 C# 类型- Python 的缩进(indentation)是语法的一部分,请注意保持一致
七、Python 与其他语言在 C# 调用方面的主要差异
下表汇总了三种语言在 PuerTS 中与 C# 交互时的语法差异,方便快速对照(完整版见 三语言对比速查表):
| 特性 | Javascript | Lua | Python |
|---|---|---|---|
| Eval 返回值 | 表达式最后一个值自动返回 | 必须使用return | lambda自动返回;def需要return |
| 匿名函数 | (a) => { ... } | function(a) ... end | lambda a: ... |
| 命名函数 | function f(a) { ... } | function f(a) ... end | def f(a): ... |
| 多行代码 | 直接写 | 直接写 | 需exec('''...''')包裹 |
| delegate 赋值 | obj.Callback = (msg) => { ... } | obj.Callback = function(msg) ... end | obj.Callback = callback_func |
| 方法调用 | 点号obj.Method() | 冒号obj:Method() | 点号obj.Method() |
| 输出到控制台 | console.log() | print() | print() |
| 空值 | null/undefined | nil | None |
| 异常抛出 | throw new Error() | error() | raise Exception() |
几个容易踩坑的要点:
print()被劫持:Python 侧(以及 Lua 侧)的print()会被 PuerTS 劫持,实际调用UnityEngine.Debug.Log输出到 Unity 控制台,无需额外配置;- 方法调用语法:JS 和 Python 统一使用点号
obj.Method(),Lua 的实例方法必须用冒号obj:Method()、静态方法用点号; Eval返回值语义:JS 自动返回最后一个表达式的值、Python 的lambda自动返回但def需要return、Lua 一律需要显式return,这是跨语言迁移时最容易出错的地方。
八、平台限制
⚠️ Python 后端当前不支持WebGL、iOS、Android 平台。如需跨平台支持,请使用 Javascript 或 Lua 后端。
这与代码层面的平台宏一致:Python 后端的原生互操作层在非 Editor 的 iOS(UNITY_IPHONE)、tvOS、WebGL、Switch 平台上被显式排除(见 unity/upms/python/Runtime/Src/Native/PapiPythonNative.cs 中的预处理指令)。因此 Python 后端仅可在桌面平台(Windows、macOS、Linux)及 Unity Editor中使用,跨平台项目请优先考虑 JavaScript 或 Lua 后端。测试用例开头也统一用#if !UNITY_WEBGL && !UNITY_IOS && !UNITY_ANDROID || FORCE_TEST_PYTHON做了平台隔离,印证了这一限制。
相关教程
- 在 C# 中调用 Javascript | 在 C# 中调用 Lua | 三语言对比速查表
- 反向教程:在 Python 中调用 C#(含
import/load_type访问类型、ref/out、泛型、迭代器、运算符重载等) - 环境搭建:安装指引 | Python 入门(runPython) | 常见问题 FAQ
【免费下载链接】puertsPUER(普洱) Typescript. Let's write your game in UE or Unity with TypeScript.项目地址: https://gitcode.com/GitHub_Trending/pu/puerts
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考