前言

作者: 👨‍💻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!";

// NOTE(timmyliang): 调用 MGlobal API 打印 Python 传递的字符串
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;
}

// NOTE(timmyliang): 定义模块的函数列表
static PyMethodDef mayaPythonCExtMethods[] = {
{"hello_world_maya", pyHelloWorldMaya, METH_VARARGS, HELLO_WORLD_MAYA_DOCSTRING},
{NULL, NULL, 0, NULL} // NOTE: (sonictk) Sentinel value for Python
};


// NOTE(timmyliang): python2 初始化函数规范 init<module_name>
#if PY_MAJOR_VERSION == 2
extern "C" PyMODINIT_FUNC initpy_hello()
{
return Py_InitModule3("py_hello",
mayaPythonCExtMethods,
MAYA_PYTHON_C_EXT_DOCSTRING);
}
// NOTE(timmyliang): python3 初始化函数规范 PyInit_<module_name>
#elif PY_MAJOR_VERSION == 3
extern "C" PyMODINIT_FUNC PyInit_py_hello()
{
static PyModuleDef hello_module = {
PyModuleDef_HEAD_INIT,
"py_hello", // Module name to use with Python import statements
MAYA_PYTHON_C_EXT_DOCSTRING, // Module description
0,
mayaPythonCExtMethods // Structure that defines the methods of the module
};
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++ 代码包含三个部分

  1. python 定义的函数
  2. 函数列表定义 (需要传入上面的 C++ 编写的 Python 函数)
  3. 模块定义 (传入上面的 函数列表)

  最后生成模块部分,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

image

导入 pyd 引入 Maya C++ 节点

  在相应的版本执行就可以看到如期触发了 maya API 的方法。
  也可以用这个方式注册 Maya 的节点和 Mel 命令,具体可以看 pyDeformer 的代码。
  只是由于没有 initializePlugin 拿不到传进来的 MObject 实例化 MFnPlugin
  我测试的 py_deformer 用了 MFnPlugin::findPlug 拿到内置插件 matrixNodes 提供的 MObject 来注册节点。
  答案是可以实现的,而且新加入的节点也会显示在 matrixNodes 上。

image

  这种骚操作不建议使用,而且也不知道会不会有什么 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();

// NOTE(TimmyLiang): python2 直接初始化模块就不会变成 built-in 模块
#if PY_MAJOR_VERSION == 2
module = Py_InitModule3("mll_py",
mayaPythonCExtMethods,
MAYA_PYTHON_C_EXT_DOCSTRING);
// NOTE(TimmyLiang): python3 用官方的方式添加模块不行,可能是因为 Py_Initialize 已经执行了
#elif PY_MAJOR_VERSION == 3
// NOTE(TimmyLiang): 参考 https://github.com/LinuxCNC/linuxcnc/issues/825 将模块加到 sys.modules 里面
static PyModuleDef hello_module = {
PyModuleDef_HEAD_INIT,
"mll_py", // Module name to use with Python import statements
MAYA_PYTHON_C_EXT_DOCSTRING, // Module description
0,
mayaPythonCExtMethods // Structure that defines the methods of the module
};

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;
}
// NOTE(timmyliang): 增加引用计数(确保不会 gc)
Py_INCREF(module);
PyGILState_Release(pyGILState);
}
return MStatus::kSuccess;
}

MStatus uninitializePlugin(MObject obj)
{
MStatus status;
// NOTE(timmyliang): 减少引用计数
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>

// https://zhuanlan.zhihu.com/p/80884925
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) #project name


file(GLOB SRCS "pybind11/*.cpp" "pybind11/*.h")

include_directories(${MAYA_INCLUDE_DIR} ${MAYA_PYTHON_INCLUDE_DIR} ${PYBIND11_INCLUDE_DIR})

link_directories(${MAYA_LIBRARY_DIR}) #specifies a directory where a linker should search for libraries

add_library(${PROJECT_NAME} SHARED ${SRCS}) #Add a dynamic library to the project using the specified source files

# pybind11_add_module(${PROJECT_NAME} ${SRCS})

target_link_libraries(${PROJECT_NAME} ${MAYA_LIBRARIES}) #specifies list of libraries to use when linking the terget and its dependents

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