如何在C++中嵌入Python且不暴露源码?求返回值获取方案
看起来你已经做了不少功课,核心需求就是在C++中调用Python逻辑、不暴露Python源码、打包成可直接调用的二进制格式(exe/dll/lib),同时还要能获取Python脚本的返回结果。下面我结合你提到的工具,给出几个可行的方案,每个方案都包含源码隐藏、返回值获取和打包的细节:
一、用C/Python API嵌入编译后的Python字节码(最灵活)
Python的.py脚本可以编译成.pyc(字节码文件),这种文件是二进制格式,直接反编译难度比源码大很多。你可以把.pyc嵌入到C++程序中,通过Python C API加载执行,完全不需要分发源码。
步骤:
编译Python脚本为字节码
用Python自带的py_compile模块编译:import py_compile py_compile.compile("your_script.py")或者用
compileall批量编译目录下的脚本:python -m compileall your_script_dir编译后会生成
__pycache__/your_script.cpython-XX.pyc(XX是Python版本号)。将字节码嵌入C++程序
把.pyc文件转换成C字节数组(可以用xxd工具:xxd -i your_script.pyc > script_data.h),然后在C中引入该头文件。调用字节码并获取返回值
示例代码:#include <Python.h> #include "script_data.h" // 包含转换后的字节码数组 // 导出给其他C++程序调用的函数(如果要打包成dll) extern "C" __declspec(dllexport) const char* RunPythonScript() { static char result_buf[1024] = {0}; Py_Initialize(); if (!Py_IsInitialized()) { strcpy(result_buf, "Python初始化失败"); return result_buf; } // 从字节数组加载编译后的代码对象 PyObject* code_obj = PyMarshal_ReadObjectFromString( reinterpret_cast<char*>(your_script_pyc), your_script_pyc_len ); if (!code_obj) { PyErr_Print(); strcpy(result_buf, "加载字节码失败"); Py_Finalize(); return result_buf; } // 获取__main__模块的命名空间 PyObject* main_module = PyImport_AddModule("__main__"); PyObject* main_dict = PyModule_GetDict(main_module); // 执行代码并获取结果 PyObject* result = PyEval_EvalCode(code_obj, main_dict, main_dict); if (result) { // 把Python对象转换成C++字符串(根据你的返回类型调整) if (PyUnicode_Check(result)) { const char* res_str = PyUnicode_AsUTF8(result); strncpy(result_buf, res_str, sizeof(result_buf)-1); } else if (PyLong_Check(result)) { sprintf(result_buf, "%lld", PyLong_AsLongLong(result)); } Py_DECREF(result); } else { PyErr_Print(); strcpy(result_buf, "脚本执行出错"); } Py_DECREF(code_obj); Py_Finalize(); return result_buf; } int main() { // 测试调用 const char* res = RunPythonScript(); printf("Python返回结果:%s\n", res); return 0; }打包成exe/dll
- 用Visual Studio编译时,链接Python的库文件(
pythonXX.lib),如果要打包成独立exe,可以静态链接Python(需要编译Python的静态库),或者把Python的dll一起打包。 - 如果编译成dll,其他C++程序可以通过
LoadLibrary和GetProcAddress调用RunPythonScript函数获取返回值。
- 用Visual Studio编译时,链接Python的库文件(
源码隐藏进阶:
如果担心.pyc被反编译,可以用第三方工具(比如pyarmor)加密字节码,然后在C++中加载时先解密字节码,再传给Python API执行。
二、用pybind11嵌入Python(现代轻量)
pybind11是比Boost.Python更轻量的绑定库,语法更简洁,适合现代C++。同样可以嵌入字节码实现源码隐藏,并且更容易处理C++和Python之间的类型转换。
步骤:
编译Python脚本为字节码(同方案一)
嵌入字节码并调用
示例代码:#include <pybind11/embed.h> #include <vector> #include <cstring> namespace py = pybind11; extern "C" __declspec(dllexport) double RunPythonCalc(double a, double b) { try { py::scoped_interpreter guard{}; // 自动管理Python解释器生命周期 // 嵌入的字节码(假设已经转换成vector) std::vector<unsigned char> pyc_data = { /* 你的pyc字节码 */ }; py::bytes pyc_bytes(reinterpret_cast<char*>(pyc_data.data()), pyc_data.size()); // 加载字节码 py::object code = py::module_::import("marshal").attr("loads")(pyc_bytes); py::object main_namespace = py::module_::import("__main__").attr("__dict__"); // 执行脚本(假设脚本里有add(a,b)函数) py::eval_code(code, main_namespace, main_namespace); py::object add_func = main_namespace["add"]; // 调用函数并获取返回值 double result = add_func(a, b).cast<double>(); return result; } catch (const py::error_already_set& e) { printf("Python错误:%s\n", e.what()); return -1; } } int main() { double res = RunPythonCalc(3.5, 2.5); printf("计算结果:%.2f\n", res); return 0; }打包
用CMake或者VS编译,链接pybind11和Python库,同样可以编译成exe或dll。pybind11支持静态编译,方便打包成独立二进制。
三、用PyInstaller打包Python脚本为dll/exe,再用C++调用
你提到了Py2exe,其实PyInstaller更强大,支持把Python脚本打包成独立的dll或者exe,然后C++可以通过调用dll的导出函数或者通过管道获取exe的输出。
方案A:打包成dll
修改Python脚本,添加导出函数
用ctypes定义导出函数:from ctypes import c_char_p, c_double def add(a: float, b: float) -> float: return a + b def get_result() -> str: return "Hello from Python!" # 定义导出接口(供PyInstaller识别) if __name__ == "__main__": import sys if sys.argv[0].endswith('.dll'): from ctypes import WINFUNCTYPE # 注册导出函数 export_add = WINFUNCTYPE(c_double, c_double, c_double)(add) export_get_result = WINFUNCTYPE(c_char_p)(get_result)用PyInstaller打包成dll
pyinstaller --name python_script --dll --export-functions add,get_result your_script.pyC++调用dll
#include <windows.h> #include <iostream> typedef double(*AddFunc)(double, double); typedef const char*(*GetResultFunc)(); int main() { HMODULE hDll = LoadLibraryA("python_script.dll"); if (!hDll) { std::cout << "加载dll失败" << std::endl; return 1; } AddFunc add = (AddFunc)GetProcAddress(hDll, "add"); GetResultFunc get_result = (GetResultFunc)GetProcAddress(hDll, "get_result"); if (add && get_result) { double res = add(5.0, 3.0); std::cout << "加法结果:" << res << std::endl; std::cout << "字符串结果:" << get_result() << std::endl; } FreeLibrary(hDll); return 0; }
方案B:打包成exe,通过管道获取返回值
如果不需要直接调用函数,只是获取脚本的输出,可以把Python脚本打包成exe,C++调用exe并读取标准输出:
#include <iostream> #include <string> #include <windows.h> std::string RunPythonExe(const std::string& exe_path) { std::string result; SECURITY_ATTRIBUTES sa; sa.nLength = sizeof(SECURITY_ATTRIBUTES); sa.bInheritHandle = TRUE; sa.lpSecurityDescriptor = NULL; HANDLE hRead, hWrite; CreatePipe(&hRead, &hWrite, &sa, 0); SetHandleInformation(hRead, HANDLE_FLAG_INHERIT, 0); STARTUPINFO si; PROCESS_INFORMATION pi; ZeroMemory(&si, sizeof(si)); si.cb = sizeof(si); si.hStdOutput = hWrite; si.hStdError = hWrite; si.dwFlags |= STARTF_USESTDHANDLES; if (CreateProcessA(NULL, const_cast<char*>(exe_path.c_str()), NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi)) { CloseHandle(hWrite); char buf[4096]; DWORD bytesRead; while (ReadFile(hRead, buf, sizeof(buf)-1, &bytesRead, NULL) && bytesRead > 0) { buf[bytesRead] = '\0'; result += buf; } CloseHandle(hRead); WaitForSingleObject(pi.hProcess, INFINITE); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); } return result; } int main() { std::string res = RunPythonExe("your_script.exe"); std::cout << "Python返回结果:\n" << res << std::endl; return 0; }
工具对比建议
- C/Python API:最底层,灵活度最高,但需要处理更多Python对象的内存管理,适合对性能和控制要求高的场景。
- pybind11:现代C++风格,语法简洁,类型转换方便,比Boost.Python轻量,推荐大多数场景使用。
- PyInstaller:快速打包,不需要写太多C++代码,适合Python逻辑复杂、快速迭代的场景,但打包后的体积可能较大。
源码隐藏注意事项
- 永远不要分发
.py源码,只用.pyc或加密后的字节码。 - 对于敏感逻辑,可以对字节码进行自定义加密,C++加载时解密后再执行,进一步提高反编译难度。
- 静态链接Python解释器可以减少依赖文件,同时避免暴露Python安装路径。
内容的提问来源于stack exchange,提问作者z-pc

