Shiboken的資料真少,僅僅為了寫一個小小的demo就大費周折。但不管怎樣,經過幾個月斷斷續續的瞭解,總算可以為純C++的庫和Qt的庫建立python的綁定了。
本文前提:
接下來記錄兩個例子:(本文例子僅在Windows和Ubuntu環境下進行過測試)
建立C++庫的綁定
步驟:
- 建立一個普通的 C++ 的庫 foo.dll 或 libfoo.so
- 使用shiboken產生膠水代碼
- 編譯膠水代碼產生綁定庫 foo.pyd 或 foo.so
- 編寫python程式進行測試
看一下本例用到的所有檔案:
|-- CMakeLists.txt||-- libfoo/| |-- CMakeLists.txt| |-- foo.h| `-- foo.cpp||-- foobinding/| |-- CMakeLists.txt| |-- foo/| | |-- CMakeLists.txt| | |-- global.h| | `-- typesystem_foo.xml| `-- tests/| |-- CMakeLists.txt| `-- test_foo.py
頂層的CMakeLists.txt 檔案內容如下:
cmake_minimum_required(VERSION 2.8)add_subdirectory(libfoo)add_subdirectory(foobinding)enable_testing()
libfoo
libfoo 中是我們原始的需要綁定的C++代碼,可以快速一下這3個檔案
project(libfoo)set(LIB_SRC foo.cpp)add_definitions("-DLIBFOO_BUILD")add_library(libfoo SHARED ${LIB_SRC})set_target_properties(libfoo PROPERTIES OUTPUT_NAME "foo")
#ifndef FOO_H#define FOO_H#if defined _WIN32 #if LIBFOO_BUILD #define LIBFOO_API __declspec(dllexport) #else #define LIBFOO_API __declspec(dllimport) #endif#else #define LIBFOO_API#endifclass LIBFOO_API Math{public: Math(){} ~Math(){} int squared(int x);};#endif // FOO_H
#include "foo.h"int Math::squared(int x){ return x * x;}
這部分沒有什麼特別的東西,直接看綁定部分
foobinding
CMakeLists.txt 檔案內容
project(foobinding) cmake_minimum_required(VERSION 2.6) find_package(PythonLibs REQUIRED)find_package(Shiboken REQUIRED) find_program(GENERATOR generatorrunner REQUIRED)if (NOT GENERATOR) message(FATAL_ERROR "You need to specify GENERATOR variable (-DGENERATOR=value)")endif() if(CMAKE_HOST_UNIX) option(ENABLE_GCC_OPTIMIZATION "Enable specific GCC flags to optimization library size and performance. Only available on Release Mode" 0) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -fvisibility=hidden -Wno-strict-aliasing") set(CMAKE_CXX_FLAGS_DEBUG "-g") if(ENABLE_GCC_OPTIMIZATION) set(CMAKE_BUILD_TYPE Release) set(CMAKE_CXX_FLAGS_RELEASE "-DNDEBUG -Os -Wl,-O1") if(NOT CMAKE_HOST_APPLE) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wl,--hash-style=gnu") endif() endif()endif() enable_testing() add_subdirectory(foo)add_subdirectory(tests)
內容本身是簡單的,就是
- 尋找python和shiboken的開發包
- 尋找可執行程式generatorrunner
- 加入兩個子目錄
只是中間加入的那堆最佳化選項是檔案看起來很亂
foo
這才是我們最重要的部分,先看CMakeLists.txt檔案:
project(foo) set(foo_SRC ${CMAKE_CURRENT_BINARY_DIR}/foo/foo_module_wrapper.cpp ${CMAKE_CURRENT_BINARY_DIR}/foo/math_wrapper.cpp) set(foo_INCLUDE_DIRECTORIES ${SHIBOKEN_INCLUDE_DIR} ${PYTHON_INCLUDE_PATH} ${libfoo_SOURCE_DIR}) set(foo_LINK_LIBRARIES ${SHIBOKEN_PYTHON_LIBRARIES} ${SHIBOKEN_LIBRARY} libfoo) include_directories(foo ${foo_INCLUDE_DIRECTORIES})add_library(foo MODULE ${foo_SRC})set_property(TARGET foo PROPERTY PREFIX "")if(WIN32) set_property(TARGET foo PROPERTY SUFFIX ".pyd")endif()target_link_libraries(foo ${foo_LINK_LIBRARIES})add_custom_command(OUTPUT ${foo_SRC} COMMAND ${GENERATOR} --generatorSet=shiboken ${CMAKE_CURRENT_SOURCE_DIR}/global.h --include-paths=${libfoo_SOURCE_DIR} --output-directory=${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/typesystem_foo.xml WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMMENT "Running generator for libfoo..." )
痛點就是產生膠水代碼(這兒的custom命令):
- 它需要一個輸入的 global.h 檔案和 typesystem_foo.xml 檔案
- 通過 --include-paths 指定global.h中包含的標頭檔所在的路徑
看一下這兩個簡單的檔案:
#include "foo.h"
<?xml version="1.0"?><typesystem package="foo"> <primitive-type name="int"/> <value-type name='Math'/></typesystem>
tests
產生東西能否工作呢?需要測試來說話:
if(WIN32) set(TEST_PYTHONPATH "${foo_BINARY_DIR}") set(TEST_LIBRARY_PATH "${libfoo_BINARY_DIR};$ENV{PATH}") set(LIBRARY_PATH_VAR "PATH") string(REPLACE "//" "/" TEST_PYTHONPATH "${TEST_PYTHONPATH}") string(REPLACE "//" "/" TEST_LIBRARY_PATH "${TEST_LIBRARY_PATH}") string(REPLACE ";" "//;" TEST_PYTHONPATH "${TEST_PYTHONPATH}") string(REPLACE ";" "//;" TEST_LIBRARY_PATH "${TEST_LIBRARY_PATH}")else() set(TEST_PYTHONPATH "${foo_BINARY_DIR}") set(TEST_LIBRARY_PATH "${libfoo_BINARY_DIR}:$ENV{LD_LIBRARY_PATH}") set(LIBRARY_PATH_VAR "LD_LIBRARY_PATH")endif()add_test(math ${SHIBOKEN_PYTHON_INTERPRETER} ${CMAKE_CURRENT_SOURCE_DIR}/test_foo.py)set_tests_properties(math PROPERTIES ENVIRONMENT "PYTHONPATH=${TEST_PYTHONPATH};${LIBRARY_PATH_VAR}=${TEST_LIBRARY_PATH}")
- 呵呵,這個檔案看起來真煩!!
- 概念上還算簡單的東西:
- 調用python來執行我們的測試程式
- 告訴python我們的綁定在那麼路徑下(通過環境變數PYTHONPATH)
- 告訴python去哪兒找需要的動態庫(環境變數Unix下LD_LIBRARY_PATH,win下PATH)
- windows下需要對路徑分隔字元進行調整(煩)
至於我們的測試程式,test_foo.py:
# -*- coding: utf-8 -*-import unittestimport fooclass MathTest(unittest.TestCase): def testMath(self): '''Test case for Math class from foo module.''' val = 5 math = foo.Math() self.assertEqual(math.squared(5), 5 * 5) if __name__ == '__main__': unittest.main()
至此,所有代碼看完了。環境準備好,編譯就比較簡單了,無非
- mkdir build
- cd build
- cmake ..
- make
- make test
建立自訂Qt庫的綁定
操作步驟和檔案位置與前面非Qt的情況完全相同:
頂層CMakeLists檔案和前面完全相同(此處略)。
libfoo
libfoo 中是我們原始的需要綁定的Qt 代碼,可以快速一下這3個檔案(與前面相比,就是多了點Qt的元素)
project(libfoo)find_package(Qt4 COMPONENTS QtCore REQUIRED)include(${QT_USE_FILE})include_directories(${CMAKE_CURRENT_BINARY_DIR})set(LIB_SRC foo.cpp)qt4_automoc(${LIB_SRC})add_definitions("-DLIBFOO_BUILD")add_library(libfoo SHARED ${LIB_SRC})target_link_libraries(libfoo ${QT_LIBRARIES})set_target_properties(libfoo PROPERTIES OUTPUT_NAME "foo")
此處使用的是qt4_automoc方式,你也可以用qt4_wrap_cpp方式(不做解釋)
#ifndef FOO_H#define FOO_H#include <QtCore/QObject>#if LIBFOO_BUILD #define LIBFOO_API Q_DECL_EXPORT#else #define LIBFOO_API Q_DECL_IMPORT#endifclass LIBFOO_API Math : public QObject{ Q_OBJECTpublic: Math(); ~Math(); int squared(int x);};#endif // FOO_H
派生自QObject,代碼整體上比前面的短,因為Q_DECL_EXPORT可以使我們避免自己根據不同平台去定義宏。
#include "foo.h"Math::Math() :QObject(NULL){}Math::~Math(){}int Math::squared(int x){ return x * x;}#include "foo.moc"
注意,最後的include語句是配合cmake的qt4_automoc工作的,如果你有在qmake下使用它的經驗,請注意它們截然不同。
接下來看綁定部分
foobinding
CMakeLists.txt 檔案內容
project(foobinding) cmake_minimum_required(VERSION 2.6) find_package(PythonLibs REQUIRED)find_package(Shiboken REQUIRED)find_package(PySide REQUIRED)find_package(Qt4 REQUIRED) include(${QT_USE_FILE})find_program(GENERATOR generatorrunner REQUIRED)if (NOT GENERATOR) message(FATAL_ERROR "You need to specify GENERATOR variable (-DGENERATOR=value)")endif() if(MSVC) set(CMAKE_CXX_FLAGS "/Zc:wchar_t- /GR /EHsc /DNOCOLOR /DWIN32 /D_WINDOWS /D_SCL_SECURE_NO_WARNINGS")else() if(CMAKE_HOST_UNIX) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -fvisibility=hidden -Wno-strict-aliasing") endif() set(CMAKE_CXX_FLAGS_DEBUG "-g") option(ENABLE_GCC_OPTIMIZATION "Enable specific GCC flags to optimization library size and performance. Only available on Release Mode" 0) if(ENABLE_GCC_OPTIMIZATION) set(CMAKE_BUILD_TYPE Release) set(CMAKE_CXX_FLAGS_RELEASE "-DNDEBUG -Os -Wl,-O1") if(NOT CMAKE_HOST_APPLE) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wl,--hash-style=gnu") endif() endif() if(CMAKE_HOST_APPLE) if (NOT QT_INCLUDE_DIR) set(QT_INCLUDE_DIR "/Library/Frameworks") endif() if(ALTERNATIVE_QT_INCLUDE_DIR) set(QT_INCLUDE_DIR ${ALTERNATIVE_QT_INCLUDE_DIR}) endif() string(REPLACE " " ":" QT_INCLUDE_DIR ${QT_INCLUDE_DIR}) endif()endif()set(GENERATOR_EXTRA_FLAGS --generator-set=shiboken --enable-parent-ctor-heuristic --enable-pyside-extensions --enable-return-value-heuristic --use-isnull-as-nb_nonzero)if(WIN32 OR DEFINED AVOID_PROTECTED_HACK) message(STATUS "PySide will be generated avoiding the protected hack!") set(GENERATOR_EXTRA_FLAGS ${GENERATOR_EXTRA_FLAGS} --avoid-protected-hack) add_definitions(-DAVOID_PROTECTED_HACK)else() message(STATUS "PySide will be generated using the protected hack!")endif()if (WIN32) set(PATH_SEP "/;")else() set(PATH_SEP ":")endif()enable_testing() add_subdirectory(foo)add_subdirectory(tests)
和它的前任比較,它複雜很多
foo
同前面一樣,此處是產生綁定的地方,也是我們最重要的部分,先看CMakeLists.txt檔案:
project(foo) set(foo_SRC ${CMAKE_CURRENT_BINARY_DIR}/foo/foo_module_wrapper.cpp ${CMAKE_CURRENT_BINARY_DIR}/foo/math_wrapper.cpp) set(foo_INCLUDE_DIRECTORIES ${SHIBOKEN_INCLUDE_DIR} ${SHIBOKEN_PYTHON_INCLUDE_DIR} ${PYSIDE_INCLUDE_DIR} ${PYSIDE_INCLUDE_DIR}/QtCore ${libfoo_SOURCE_DIR}) set(foo_LINK_LIBRARIES ${SHIBOKEN_PYTHON_LIBRARIES} ${SHIBOKEN_LIBRARY} ${PYSIDE_LIBRARY} ${QT_LIBRARIES} libfoo) include_directories(foo ${foo_INCLUDE_DIRECTORIES})add_library(foo MODULE ${foo_SRC})set_property(TARGET foo PROPERTY PREFIX "")if(WIN32) set_property(TARGET foo PROPERTY SUFFIX ".pyd")endif()target_link_libraries(foo ${foo_LINK_LIBRARIES})add_custom_command(OUTPUT ${foo_SRC} COMMAND ${GENERATOR} --generatorSet=shiboken ${GENERATOR_EXTRA_FLAGS} ${CMAKE_CURRENT_SOURCE_DIR}/global.h --include-paths=${libfoo_SOURCE_DIR}${PATH_SEP}${QT_INCLUDE_DIR}${PATH_SEP}${PYSIDE_INCLUDE_DIR} --output-directory=${CMAKE_CURRENT_BINARY_DIR} --typesystem-paths=${typesystem_path}${PATH_SEP}${PYSIDE_TYPESYSTEMS} ${CMAKE_CURRENT_SOURCE_DIR}/typesystem_foo.xml WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMMENT "Running generator for libfoo..." )
注意和它前任比較,變複雜的部分:
看一下另外兩個檔案:
#undef QT_NO_STL#undef QT_NO_STL_WCHAR #ifndef NULL#define NULL 0#endif #include "pyside_global.h"#include "foo.h"
幾個宏看不太懂,多了一個pyside_global.h檔案,屬於PySide內建的檔案
<?xml version="1.0"?><typesystem package="foo"> <load-typesystem name="typesystem_core.xml" generate="no"/> <object-type name='Math' /></typesystem>
需要載入PySide 的 typesystem_core.xml
tests
產生東西能否工作,仍然需要測試來說話:
if(WIN32) set(TEST_PYTHONPATH "${foo_BINARY_DIR};${PYSIDE_PYTHONPATH}") set(TEST_LIBRARY_PATH "${libfoo_BINARY_DIR};$ENV{PATH}") set(LIBRARY_PATH_VAR "PATH") string(REPLACE "//" "/" TEST_PYTHONPATH "${TEST_PYTHONPATH}") string(REPLACE "//" "/" TEST_LIBRARY_PATH "${TEST_LIBRARY_PATH}") string(REPLACE ";" "//;" TEST_PYTHONPATH "${TEST_PYTHONPATH}") string(REPLACE ";" "//;" TEST_LIBRARY_PATH "${TEST_LIBRARY_PATH}")else() set(TEST_PYTHONPATH "${foo_BINARY_DIR}:${PYSIDE_PYTHONPATH}") set(TEST_LIBRARY_PATH "${libfoo_BINARY_DIR}:$ENV{LD_LIBRARY_PATH}") set(LIBRARY_PATH_VAR "LD_LIBRARY_PATH")endif()add_test(math ${SHIBOKEN_PYTHON_INTERPRETER} ${CMAKE_CURRENT_SOURCE_DIR}/test_foo.py)set_tests_properties(math PROPERTIES ENVIRONMENT "PYTHONPATH=${TEST_PYTHONPATH};${LIBRARY_PATH_VAR}=${TEST_LIBRARY_PATH}")
恩,基本沒變化,就是多了個PYSIDE_PYTHONPATH
至於 test_foo.py 檔案,和第一部分完全一樣,此處略。