我直接在Qt的例子程序textedit里面增加代码。
CPython C API的文档非常晦涩,而且没有好的教程。API的解释不容易看懂,也没有例子辅助理解。看了很多文章,终于成功嵌入python解释器,并增加自定义模块。
本例主要使用PyCXX这个辅助库来编写,感觉比较方便。具体和boost.python,PyBind11谁更好,我不清楚,但是boost的模板有时让人恐惧,出了问题会很麻烦。本例也主要参考了PyCXX的示例来写的,FreeCAD就是使用PyCXX嵌入python解释器的。
由于TextEdit类并没有什么值得暴露的方法和属性,我选择了向python添加QTextEdit的append方法。
由于windows下CPython使用特定版本的Visual Studio,所以写扩展模块是必须选择与CPython编译时同样的Visual Studio 版本。
CPython 与微软 Visual Studio 各版本对应关系
注意这个表格并不权威,有时同一版本不同时期发布的python解释器使用了不同版本的Visual Studio。
Python | 微软 | Visual Studio |
---|---|---|
v3.8 | v2017 | MSC v.1912 64 bit (AMD64) |
v3.7 | v2017 | MSC v.1912 64 bit (AMD64) |
v3.6 | v2015 | MSC v.1900 64 bit (AMD64) |
v3.5 | v2015 | MSC v.1900 64 bit (AMD64) |
v3.4 | v2010 | MSC v.1600 64 bit (AMD64) |
v2.7 | v2008 | MSC v.1500 64 bit (AMD64) |
main.cpp
#include "textedit.h"
#include <QApplication>
...
// 这是我增加的python类型
#include "PyTextEdit.h"
// 这是我增加的python模块
#include "AppPythonModule.h"
int main(int argc, char *argv[])
{
Q_INIT_RESOURCE(textedit);
QApplication a(argc, argv);
...
TextEdit mw;
...
// 目录应该放在配置文件,如果不设置PYTHONHOME,Py_Initialize会引起程序崩溃。
Py_SetPythonHome(L"C:\\Python37");
Py_Initialize();
// 增加自定义模块App
PyObject* modules = PyImport_GetModuleDict();
PyObject* pAppModule = PyInit_App();
PyDict_SetItemString(modules, "App", pAppModule);
mw.show();
int res = a.exec();
Py_Finalize();
return res;
}
PyTextEdit.h
#pragma once
#pragma push_macro("slots")
#undef slots
#include "CXX/Objects.hxx"
#include "CXX/Extensions.hxx"
#pragma pop_macro("slots")
#include <qglobal.h>
QT_BEGIN_NAMESPACE
class QTextEdit;
QT_END_NAMESPACE
void setTextEdit(QTextEdit *textEdit);
class PyTextEdit : public Py::PythonClass<PyTextEdit>
{
public:
PyTextEdit(Py::PythonClassInstance *self, Py::Tuple &args, Py::Dict &kwds);
~PyTextEdit();
static void init_type(void);
Py::Object getattro(const Py::String &name_) override;
int PyTextEdit::setattro(const Py::String &name_, const Py::Object &value) override;
Py::Object append(const Py::Tuple &args);
PYCXX_VARARGS_METHOD_DECL( PyTextEdit, append )
private:
Py::String m_value;
};
PyTextEdit.cpp
#include "PyTextEdit.h"
#include <QTextEdit>
QTextEdit *g_textEdit = nullptr;
void setTextEdit(QTextEdit *textEdit)
{
g_textEdit = textEdit;
}
PyTextEdit::PyTextEdit(Py::PythonClassInstance *self, Py::Tuple &args, Py::Dict &kwds)
: Py::PythonClass< PyTextEdit >( self, args, kwds )
{
}
PyTextEdit::~PyTextEdit()
{
}
void PyTextEdit::init_type(void)
{
behaviors().name( "PyTextEdit" );
behaviors().doc( "documentation for PyTextEdit class" );
behaviors().supportGetattro();
behaviors().supportSetattro();
PYCXX_ADD_VARARGS_METHOD( append, append, "docs for append" );
// Call to make the type ready for use
behaviors().readyType();
}
Py::Object PyTextEdit::getattro( const Py::String &name_ )
{
std::string name( name_.as_std_string( "utf-8" ) );
if( name == "value" )
{
return m_value;
}
else
{
return genericGetAttro( name_ );
}
}
int PyTextEdit::setattro( const Py::String &name_, const Py::Object &value )
{
std::string name( name_.as_std_string( "utf-8" ) );
if( name == "value" )
{
m_value = value;
return 0;
}
else
{
return genericSetAttro( name_, value );
}
}
Py::Object PyTextEdit::append( const Py::Tuple &args )
{
Py::String line = args[0];
if (g_textEdit) {
auto ustr = line.as_unicodestring();
g_textEdit->append(QString::fromWCharArray(ustr.c_str()));
}
return Py::None();
}
AppPythonModule.h
#pragma once
#pragma push_macro("slots")
#undef slots
#include "CXX/Objects.hxx"
#include "CXX/Extensions.hxx"
#pragma pop_macro("slots")
class AppPythonModule : public Py::ExtensionModule<AppPythonModule>
{
public:
AppPythonModule();
~AppPythonModule();
private:
Py::Object makeTextEdit(const Py::Tuple &args, const Py::Dict &kwds);
};
#define EXPORT_SYMBOL
extern "C" EXPORT_SYMBOL PyObject *PyInit_App();
// symbol required for the debug version
extern "C" EXPORT_SYMBOL PyObject *PyInit_App_d();
AppPythonModule.cpp
#include "AppPythonModule.h"
#include "PyTextEdit.h"
AppPythonModule::AppPythonModule()
: Py::ExtensionModule<AppPythonModule>( "App" ) // this must be name of the file on disk e.g. app.so or app.pyd
{
PyTextEdit::init_type();
auto type = *PyTextEdit::type();
// after initialize the moduleDictionary will exist
initialize( "documentation for the App module" );
// 添加自定义类型TextEdit
PyModule_AddObject(this->module().ptr(), "TextEdit", (PyObject *)type);
Py::Dict d( moduleDictionary() );
Py::TupleN args;
Py::Dict kwds;
// 添加模块属性textEdit
d["textEdit"] = makeTextEdit(args, kwds);
}
AppPythonModule::~AppPythonModule()
{
}
Py::Object AppPythonModule::makeTextEdit( const Py::Tuple &args, const Py::Dict &kwds )
{
std::cout << "makeTextEdit Called with " << args.length() << " normal arguments." << std::endl;
Py::List names( kwds.keys() );
std::cout << "and with " << names.length() << " keyword arguments:" << std::endl;
for( Py::List::size_type i=0; i< names.length(); i++ )
{
Py::String name( names[i] );
std::cout << " " << name << std::endl;
}
Py::Callable class_type( PyTextEdit::type() );
Py::PythonClassObject<PyTextEdit> textEdit( class_type.apply( args, kwds ) );
return textEdit;
}
static AppPythonModule *app;
extern "C" EXPORT_SYMBOL PyObject *PyInit_App()
{
#if defined(PY_WIN32_DELAYLOAD_PYTHON_DLL)
Py::InitialisePythonIndirectPy::Interface();
#endif
app = new AppPythonModule;
return app->module().ptr();
}
// symbol required for the debug version
extern "C" EXPORT_SYMBOL PyObject *PyInit_App_d()
{
return PyInit_App();
}
下面是我点击按钮时运行的测试python代码:
void TextEdit::runPython()
{
// 必须使用utf8编码,否则报告下面错误。
// SyntaxError: (unicode error) 'utf-8' codec can't decode byte 0xd5 in position 0: invalid continuation
PyRun_SimpleString(u8R"raw(
import App
App.textEdit.value = "hello"
App.textEdit.append("这是python解释器添加的文本。\n");
textEdit2 = App.TextEdit()
textEdit2.append("这是python解释器通过自定义类型添加的文本。\n");
)raw");
}
结果图展示:
线程问题还没考虑。暂时就在主线程上跑的python解释器。如果python代码太耗时,应该会导致界面冻结。如果在其他线程跑,需要使用Qt的signal机制和主线程通信更新界面。
另外在多个线程跑python代码的同步问题也没有在这个例子中演示。
PyCXX类简介
- CXX\Python3\Objects.hxx
Py::Object是PyObject*的包装类。它的派生类(Boolean, Bytes, Dict, Float, List, Long, String, Tuple, Type)包装了Python的固有类型。TupleN是Tuple的派生类,增加了构造Tuple的便利构造函数。
- CXX\Python3\ExtensionType.hxx
PyClass这个类基本就是实现自定义类型的PyTypeObject对象,它又是通过PythonType来实现的。
template<TEMPLATE_TYPENAME T>
class PythonClass : public PythonExtensionBase
{
protected:
explicit PythonClass( PythonClassInstance *self, Tuple &/*args*/, Dict &/*kwds*/ )
: PythonExtensionBase()
, m_class_instance( self )
{
}
...
static ExtensionClassMethodsTable &methodTable()
{
static ExtensionClassMethodsTable *method_table;
if( method_table == NULL )
method_table = new ExtensionClassMethodsTable;
return *method_table;
}
static void add_method( const char *name, PyCFunction function, int flags, const char *doc=NULL )
{
behaviors().set_methods( methodTable().add_method( name, function, flags, doc ) );
}
static PythonType &behaviors()
{
static PythonType *p;
if( p == NULL )
{
#if defined( _CPPRTTI ) || defined( __GNUG__ )
const char *default_name = (typeid( T )).name();
#else
const char *default_name = "unknown";
#endif
p = new PythonType( sizeof( PythonClassInstance ), 0, default_name );
p->set_tp_new( extension_object_new );
p->set_tp_init( extension_object_init );
p->set_tp_dealloc( extension_object_deallocator );
// we are a class
p->supportClass();
// always support get and set attr
p->supportGetattro();
p->supportSetattro();
}
return *p;
}
public:
static PyTypeObject *type_object()
{
return behaviors().type_object();
}
static Object type()
{
return Object( reinterpret_cast<PyObject *>( behaviors().type_object() ) );
}
...
virtual Object self()
{
return Object( reinterpret_cast<PyObject *>( m_class_instance ) );
}
private:
PythonClassInstance *m_class_instance;
};
PyClassInstance这个类是你自定义类型的Python对象。它很简单,而且不是类模板。
struct PythonClassInstance
{
PyObject_HEAD
PythonExtensionBase *m_pycxx_object;
};
PyClassObject是一个简单的包装类,包装了PyClassInstance对象。就是Py::Long包装了Python的整数PyLongObject一样。
你要做的事就是从PythonClass派生实现自定义python类型,填充PyTypeObject的事情就交给PythonClass。
class PyTextEdit : public Py::PythonClass<PyTextEdit>
添加自定义类型TextEdit
PyModule_AddObject(this->module().ptr(), "TextEdit", (PyObject *)type);
- PythonType.hxx
PythonType这个类是为了操作PyTypeObject。
PYCXX_VARARGS_METHOD_DECL宏把实例方法包装成静态方法,因为CPython最终调用的都是这样的静态方法。