前言 作者: 👨💻sonictk
https://github.com/sonictk/maya_python_c_extension
这篇文章也是参考 sonictk 大佬的提供的 pyd 开发文章。 文章也提到之前的 hot reload 方案已经解决了很多 C++ 开发困难的问题。 然而还是有很多情况需要开发一个 python 的 C++ 模块实现 Maya C++ API 的 调用。 这个情况有点像是 Unreal 暴露 C++ API 到 Python 一样。
Maya 编译 c 相关 Python 库 & pyd 编译
之前我也写过关于 Maya pyd 编译的文章,但是这个文章是用 Cython 自动生成 C 代码编译实现的,这次是手写 pyd。
什么是 pyd
pyd 本质上也是一个 dll 文件,就像 Maya 插件的 mll 一样。 只是 pyd 规定了一些暴露规则,从而让 python 解释器可以读取。 这也是 Python 称之为胶水语言的一大特点,它可以无缝和 C++ 编译的模块进行交互。 因此很多 C++ 的包 比如 Qt 等可以暴露接口到 Python 实现调用。
pyd hello world 案例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 #include <Python.h> #include <maya/MGlobal.h> #include <stdio.h> static const char MAYA_PYTHON_C_EXT_DOCSTRING[] = "An example Python C extension that makes use of Maya functionality." ;static const char HELLO_WORLD_MAYA_DOCSTRING[] = "Says hello world!" ;static PyObject *pyHelloWorldMaya (PyObject *module , PyObject *args) { const char *inputString; if (!PyArg_ParseTuple (args, "s" , &inputString)) { return NULL ; } PyGILState_STATE pyGILState = PyGILState_Ensure (); MGlobal::displayInfo (inputString); PyObject *result = Py_BuildValue ("s" , inputString); PyGILState_Release (pyGILState); return result; } static PyMethodDef mayaPythonCExtMethods[] = { {"hello_world_maya" , pyHelloWorldMaya, METH_VARARGS, HELLO_WORLD_MAYA_DOCSTRING}, {NULL , NULL , 0 , NULL } }; #if PY_MAJOR_VERSION == 2 extern "C" PyMODINIT_FUNC initpy_hello () { return Py_InitModule3 ("py_hello" , mayaPythonCExtMethods, MAYA_PYTHON_C_EXT_DOCSTRING); } #elif PY_MAJOR_VERSION == 3 extern "C" PyMODINIT_FUNC PyInit_py_hello () { static PyModuleDef hello_module = { PyModuleDef_HEAD_INIT, "py_hello" , MAYA_PYTHON_C_EXT_DOCSTRING, 0 , mayaPythonCExtMethods }; return PyModule_Create (&hello_module); } #endif
上面的代码就是一个小案例,将 C++ 编译成 pyd 给 python 调用。 并且这里引用了 Maya 的 API ,因此只能使用 Maya 的 Python Interpreter (mayapy.exe) 进行加载。 如果使用其他 Python 导入这个模块会出现如下的错误
1 2 3 4 Traceback (most recent call last): File "d:/Obsidian/Personal/2 _Area/📝Blog/CG/Maya/C++/test_load.py", line 5 , in <module> import py_hello ImportError: DLL load failed while importing py_hello : 找不到指定的程序。
pyd 的 C++ 代码包含三个部分
python 定义的函数
函数列表定义 (需要传入上面的 C++ 编写的 Python 函数)
模块定义 (传入上面的 函数列表)
最后生成模块部分,Python2 和 Python3 暴露的 API 不一致,可以用宏来区分。
编译这个 cpp 需要加上 Maya include 目录的头文件,以及链接 Maya lib 的静态库文件。 另外编译 pyd 需要特别注意的是,它也需要想 mll 一样暴露出初始化的函数。 在 python2 下是 init<module_name>
开头,在 python3 下是 PyInit_<module_name>
开头。 在 cpp 里面配置编译环境是个相当让人头疼的问题。 我在自己的 CMakeMaya 库里面已经配置好了编译用的环境, 具体的使用方法可以看 readme 或者参考我的文章 Maya CMake 构建 C++ 插件编译环境
在我提供的环境下执行 doit c -p pyd -v 2020
即可编译出 pyd 到 plug-ins\Release\maya2022\pyd\py_hello.pyd
需要注意 pyd 在不同的平台不同Maya版本都需要单独编译。这里我提供了编译好给 Windows64 Maya2020 的 pyd
导入 pyd 引入 Maya C++ 节点
在相应的版本执行就可以看到如期触发了 maya API 的方法。 也可以用这个方式注册 Maya 的节点和 Mel 命令,具体可以看 pyDeformer 的代码。 只是由于没有 initializePlugin
拿不到传进来的 MObject
实例化 MFnPlugin
。 我测试的 py_deformer 用了 MFnPlugin::findPlug
拿到内置插件 matrixNodes
提供的 MObject 来注册节点。 答案是可以实现的,而且新加入的节点也会显示在 matrixNodes
上。
这种骚操作不建议使用,而且也不知道会不会有什么 BUG 导致 Maya 崩溃。 另外没有办法触发 uninitializePlugin
来注销这个节点的注册。
pyd mll 缝合怪
基于上面的测试我发现还可以生成出既是 Maya 插件又是 Python 模块的 缝合怪文件。 因为 C++ 只要编译的时候 export 出对应的方法就可以加载。
只是 Python 加载二进制包要求文件后缀为 pyd ,Maya 加载二进制插件要求文件命名为 mll 才可以。 解决这个问题,可以用软连接或者拆分成两个文件来实现,经过测试是可以的,具体可以看 pyCommand 的 测试代码 。
使用 mll 嵌入 python 模块
上面主要实现按照 python 的规范加载包的操作,sonitck
的文章还提供了一个方案,加载 mll 获取到 python 包的方式。 做法也不复杂,就是在 initializePlugin
的时候加上加上 C++ 的模块。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 #include <Python.h> #include <maya/MFnPlugin.h> #include <maya/MGlobal.h> const char *kAUTHOR = "TimmyLiang" ;const char *kVERSION = "1.0.0" ;const char *kREQUIRED_API_VERSION = "Any" ;static const char HELLO_WORLD_MAYA_DOCSTRING[] = "Says hello world!" ;static const char MAYA_PYTHON_C_EXT_DOCSTRING[] = "An example Python C extension that makes use of Maya functionality." ;PyObject *module = NULL ; static PyObject *pyHelloWorldMaya (PyObject *module , PyObject *args) { const char *inputString; if (!PyArg_ParseTuple (args, "s" , &inputString)) { return NULL ; } PyGILState_STATE pyGILState = PyGILState_Ensure (); MGlobal::displayInfo (inputString); PyObject *result = Py_BuildValue ("s" , inputString); PyGILState_Release (pyGILState); return result; } static PyMethodDef mayaPythonCExtMethods[] = { {"hello_world_maya" , pyHelloWorldMaya, METH_VARARGS, HELLO_WORLD_MAYA_DOCSTRING}, {NULL , NULL , 0 , NULL } }; MStatus initializePlugin (MObject obj) { MFnPlugin plugin (obj, kAUTHOR, kVERSION, kREQUIRED_API_VERSION) ; if (!Py_IsInitialized ()) Py_Initialize (); if (Py_IsInitialized ()) { PyGILState_STATE pyGILState = PyGILState_Ensure (); #if PY_MAJOR_VERSION == 2 module = Py_InitModule3 ("mll_py" , mayaPythonCExtMethods, MAYA_PYTHON_C_EXT_DOCSTRING); #elif PY_MAJOR_VERSION == 3 static PyModuleDef hello_module = { PyModuleDef_HEAD_INIT, "mll_py" , MAYA_PYTHON_C_EXT_DOCSTRING, 0 , mayaPythonCExtMethods }; module = PyModule_Create (&hello_module); PyObject *sys_modules = PyImport_GetModuleDict (); PyDict_SetItemString (sys_modules, "mll_py" , module ); #endif MGlobal::displayInfo ("Registered Python bindings!" ); if (module == NULL ) { return MStatus::kFailure; } Py_INCREF (module ); PyGILState_Release (pyGILState); } return MStatus::kSuccess; } MStatus uninitializePlugin (MObject obj) { MStatus status; Py_DECREF (module ); return status; }
上面的代码兼容 python2 python3 版本。 python2 直接用默认的 Py_InitModule
方法就可以添加,如果在 Python 打印模块会提示 <module 'mll_py' (built-in)>
但是 python3 下面不行,后来查找了 Github 的 issue 通过将模块添加到 sys.modules
下面解决问题。 只是模块打印就是普通的模块。 那为什么将模块放到 sys.modules 就可以了,这 Python 的 import 机制有关。 Python - Import 机制
这个方式可以将一些 C++ 的 API 暴露给 Python,只是这个操作需要更多的说明。 否则没人知道这个 mll 居然添加一个 Python 模块。
pybind11 自动绑定
通过上面一顿操作,也可以深刻体会到如果跨版本兼容 C++ 需要做很多宏的判断,相当繁琐。 包括 Python2 和 Python3 暴露的方法名不一样,需要在 CMake 上进行判断。 使用 pybind11 进行转换相对方便许多
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <Python.h> #include <maya/MGlobal.h> #include <stdio.h> #include <pybind11/pybind11.h> void displayInfo (char *inputString) { MGlobal::displayInfo (inputString); return ; } PYBIND11_MODULE ( pybind11cpp, m ){ m.doc () = "pybind11 example" ; m.def ("display_info" , &displayInfo, "Maya Display Info" ,pybind11::arg ("inputString" ) = "hello world!" ); }
pybind11 会自动将 Python 的参数进行转换 这样只要将纯粹的 C++ 函数放入到 PYBIND11_MODULE
宏 并且 pybind11 的 2.9 版本支持 python2 python3 的 pyd 编译。 只要在 cmake 里面配置 /export
对应的方法即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 find_package (Pybind11 REQUIRED) project (pybind11cpp) file (GLOB SRCS "pybind11/*.cpp" "pybind11/*.h" )include_directories (${MAYA_INCLUDE_DIR} ${MAYA_PYTHON_INCLUDE_DIR} ${PYBIND11_INCLUDE_DIR} )link_directories (${MAYA_LIBRARY_DIR} ) add_library (${PROJECT_NAME} SHARED ${SRCS} ) target_link_libraries (${PROJECT_NAME} ${MAYA_LIBRARIES} ) if (${MAYA_VERSION} GREATER 2020 ) set (PYBIND_LINK_FLAGS "/export:PyInit_pybind11cpp" ) else () set (PYBIND_LINK_FLAGS "/export:initpybind11cpp" ) endif ()set_target_properties (${PROJECT_NAME} PROPERTIES LINK_FLAGS ${PYBIND_LINK_FLAGS} SUFFIX ".pyd" )
pybind11 可以使用 pybind11_add_module
来生成 pyd 但是它是自动查找 Python 环境,指定 Maya 的 Python 需要额外的配置。 所以我就不用这个,自己来配置好了。
通过上面的方式可以大大简化 C++ 的编写。
总结
以上就是 pyd 编译的各种折腾结果。 社区里面值得说道的有 cmdc 基于 pybind11 编译的二次封装 C++ API 库。
Python 调用 C++ 还有利用 ctypes
库访问 dll 的方式 后续也可以实验一下在 Python 中从 dll 里面调用 function 实现 参考:https://github.com/Autodesk/animx