PuerTS 3.0 实战:在 C 中调用 Python —— Delegate 桥接、参数传递、返回值与异常处理全解析
发布时间:2026/9/17 10:20:13 锦皓数字建站

PuerTS 3.0 实战在 C# 中调用 Python —— Delegate 桥接、参数传递、返回值与异常处理全解析【免费下载链接】puertsPUER(普洱) Typescript. Lets 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三者共用统一的ScriptEnvBackend架构仅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.Evalstring(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.Evalstring(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.Actionint LogInt env.EvalSystem.Actionint(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.Actionint LogInt env.EvalSystem.Actionint(log_int); LogInt(3); // Output: 3 env.Dispose(); }2.3 可选参数与多签名 delegatePython 函数还支持可选参数转换为不同签名的 delegate 后都可以正常工作void Start() { var env new Puerts.ScriptEnv(new Puerts.BackendPython()); env.Eval( exec( def flexible_func(a, b0): if b 0: return str(a) else: return str(a) str(b) ) ); // Cast as Actionint — only pass the first argument var cb1 env.EvalActionint(flexible_func); cb1(1); // Uses default b0 // Cast as Actionstring, long — pass both arguments var cb2 env.EvalActionstring, long(flexible_func); cb2(hello, 999); // Output: hello999 env.Dispose(); }需要注意的是如果你生成的 delegate 带有值类型参数需要添加UsingAction或者UsingFunc声明。具体请参见 FAQ值类型参数如int、long、struct意味着底层需要反射生成对应的 delegate bridge在 IL2CPP 等裁剪环境下会失败必须先通过JsEnv.UsingActionT1, T2...()无返回值或JsEnv.UsingFuncT1, 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.Funcint, int Add3 env.EvalSystem.Funcint, 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.Funcint, int Add3 env.EvalSystem.Funcint, int(add3); System.Console.WriteLine(Add3(1)); // Output: 4 env.Dispose(); }3.3 直接使用EvalT获取简单返回值如果你只是需要某个 Python 表达式的计算结果可以直接用泛型EvalTvoid Start() { var env new Puerts.ScriptEnv(new Puerts.BackendPython()); // Directly evaluate a Python expression and get the return value int result env.Evalint(1 2); System.Console.WriteLine(result); // Output: 3 string str env.Evalstring(hello python); System.Console.WriteLine(str); // Output: hello python // Convert non-string types with Python builtins var ret env.Evalstring(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.EvalSystem.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.csDispose()会销毁后端创建的解释器环境引用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.EvalActionMonoBehaviour(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.EvalActionMonoBehaviour(init_behaviour)取回 Python 函数并传入this完成绑定生命周期管理OnDestroy中把三个 delegate 全部置空与第五节的生命周期注意事项相呼应。⚠️ 注意 Python 与其他语言的关键差异Python 多行代码需要exec(...)包裹Python 使用def定义函数无需end或花括号Python 使用import语法访问 C# 类型Python 的缩进indentation是语法的一部分请注意保持一致七、Python 与其他语言在 C# 调用方面的主要差异下表汇总了三种语言在 PuerTS 中与 C# 交互时的语法差异方便快速对照完整版见 三语言对比速查表特性JavascriptLuaPythonEval 返回值表达式最后一个值自动返回必须使用returnlambda自动返回def需要return匿名函数(a) { ... }function(a) ... endlambda a: ...命名函数function f(a) { ... }function f(a) ... enddef f(a): ...多行代码直接写直接写需exec(...)包裹delegate 赋值obj.Callback (msg) { ... }obj.Callback function(msg) ... endobj.Callback callback_func方法调用点号obj.Method()冒号obj:Method()点号obj.Method()输出到控制台console.log()print()print()空值null/undefinednilNone异常抛出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 的 iOSUNITY_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. Lets write your game in UE or Unity with TypeScript.项目地址: https://gitcode.com/GitHub_Trending/pu/puerts创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。