CMake と SWIG を使って C++ と Python と ROOT に対応させる

やりたいこと

CTA の焦点面カメラの開発に必要なソフトを新たに書く必要があり、以下の条件を満たす必要があります。

  • 速度重視の用途に耐えるため、また multi thread に対応するため、中身は C++ で書かれていること
  • 速度を重視しない場合や簡単な試験にすぐ使えるように、Python からも利用可能なこと
  • C++ を使えない人にも使いやすいこと (これもすなわち、Python で動くこと)
  • ROOT からも動くこと
  • OS X でも Linux でも動くこと
  • Engineer の人でも使えるよう、将来的に Windows 対応もできること
  • ROOT に不慣れな人、ROOT を install していない人でも動かせること
  • PyROOT ではなく、純粋に Python のみで動くこと

つまり、標準では C++ の shared library を生成し、必要に応じて Python 用の library も生成し、さらに必要に応じて ROOT 用の library も生成するものが必要です。動作環境も全部載せです。

解決策



結論として、CMake と SWIG を使うことにしました。

まず、Autoconf を今更勉強したくなかった*1ので、CMake にしました。それと、CMake のほうが文法が簡単そうというのも理由です*2。また、最近の Geant4 では CMake が標準になったのも理由です。ROOT も新しいものは CMake に対応しています。Geant4 と ROOT が CMake に移行しているのだから、CMake の勉強しておけば役に立つだろう、と。

次に、C++ で書かれた code を Python に持って行くには、やはり SWIG が標準的だそうで、SWIG を使うことにしました。CMake から SWIG を呼び出すのも簡単なことが分かったのも理由です。

簡単と言えば簡単なのですが、CMake の分かりやすい解説はそれほど整備されていません。数日間にわたる Google 検索と試行錯誤の結果をここにまとめます。

使い方

階層

どのように directory や file を置くかは好みですが、自分の試した構成は次の通りです。実際にはもっと inc と src の中身は増えます。

target+
      |-CMakeLists.txt
      |-inc+
      |    |-BasePacket.h
      |    |-CommandPacket.h
      |    |-ResponsePacket.h
      |    |-LinkDef.h
      |
      |-src+
           |-BasePacket.cxx
           |-CommandPacket.cxx
           |-ResponsePacket.cxx
           |-CMakeLists.txt
           |-target.i
それぞれの中身
cmake_minimum_required(VERSION 2.6 FATAL_ERROR)

project(TARGET)
set(TARGET_VERSION_MAJOR 1)
set(TARGET_VERSION_MINOR 0)

option(PYTHON "Build the Python version of TARGET library" OFF)
option(ROOT "Build the ROOT version of TARGET library" OFF)

subdirs(src)

include_directories("${TARGET_SOURCE_DIR}/inc")

▲ target/CMakeLists.txt

aux_source_directory(. SOURCES)

add_library(TARGET SHARED ${SOURCES})
install(TARGETS TARGET DESTINATION ${CMAKE_INSTALL_PREFIX}/lib)

if(ROOT)
  message("ROOT support is added")

  find_package(ROOT REQUIRED)
  include(${ROOT_USE_FILE})

  include_directories(${ROOT_INCLUDE_DIRS})

  file(GLOB INCS "${TARGET_SOURCE_DIR}/inc/*h")
  file(GLOB LINKDEF_H "${TARGET_SOURCE_DIR}/inc/LinkDef.h")
  list(REMOVE_ITEM INCS ${LINKDEF_H})

  ROOT_GENERATE_DICTIONARY(RTARGETDict ${INCS} LINKDEF ${LINKDEF_H})
  ROOT_LINKER_LIBRARY(RTARGET ${SOURCES} RTARGETDict.cxx LIBRARIES Hist MathCore)
  ROOT_GENERATE_ROOTMAP(RTARGET LINKDEF ${LINKDEF_H} DEPENDENCIES Hist MathCore)
endif(ROOT)

if(PYTHON)
  message("Python support is added")
  find_package(SWIG REQUIRED)
  find_package(PythonLibs REQUIRED)

  include(${SWIG_USE_FILE})
  set(CMAKE_SWIG_FLAGS "")
  include_directories(${PYTHON_INCLUDE_DIRS})

  set_source_files_properties(target.i PROPERTIES CPLUSPLUS ON)
  set_source_files_properties(target.i PROPERTIES SWIG_FLAGS "-includeall")
  swig_add_module(target python target.i ${SOURCES})
  swig_link_libraries(target ${PYTHON_LIBRARIES})

  execute_process(COMMAND python -c "from distutils.sysconfig import get_python_lib; print get_python_lib()" OUTPUT_VARIABLE PYTHON_SITE_PACKAGES OUTPUT_STRIP_TRAILING_WHITESPACE)
  install(TARGETS _target DESTINATION ${PYTHON_SITE_PACKAGES})
  install(FILES ${CMAKE_BINARY_DIR}/src/target.py DESTINATION ${PYTHON_SITE_PACKAGES})
endif(PYTHON)

▲ src/CMakeLists.txt

#ifndef TARGET_BASE_PACKET_H
#define TARGET_BASE_PACKET_H

namespace TARGET {

class BasePacket
{
private:
public:
  BasePacket();
  virtual ~BasePacket();
};

} // TARGET

#endif // TARGET_BASE_PACKET_H

▲ target/inc/BasePacket.h

#include "BasePacket.h"

namespace TARGET {

BasePacket::BasePacket()
{
}

//______________________________________________________________________________
BasePacket::~BasePacket()
{
}

} // TARGET

▲ target/src/BasePacket.cxx (中身はまだ空です)

#ifdef __CINT__

#pragma link off all globals;
#pragma link off all classes;
#pragma link off all functions;

#pragma namespace TARGET;

#pragma link C++ class TARGET::BasePacket;
#pragma link C++ class TARGET::CommandPacket;
#pragma link C++ class TARGET::ResponsePacket;

#endif

▲ inc/LinkDef.h

%module target
%{
#include "BasePacket.h"
#include "CommandPacket.h"
#include "ResponsePacket.h"
%}
%include "BasePacket.h"
%include "CommandPacket.h"
%include "ResponsePacket.h"

▲ src/target.i

Build

$HOME に target という directory があると仮定します。

$ mkdir build
$ cd build
$ cmake ~/target -DPYTHON=ON -DROOT=ON
(snip)
$ make
(snip)
$ sudo make install
[ 35%] Built target RTARGET
[ 57%] Built target TARGET
[ 92%] Built target _target
[100%] Built target libRTARGET.rootmap
Install the project...
-- Install configuration: "RelWithDebInfo"
-- Installing: /usr/local/lib/libTARGET.dylib
-- Installing: /usr/local/lib/libRTARGET.so
-- Installing: /usr/local/lib/libRTARGET.rootmap
-- Installing: /Library/Python/2.7/site-packages/_target.so
-- Installing: /Library/Python/2.7/site-packages/target.py

これで、/usr/local/lib に C++ 用の library である libTARGET.dylib、ROOT 用の libRTARGET.so と libRTARGET.rootmap、また /Library/Python/2.7/site-packages に Python 用の _target.so と target.py が install されました。

実際に使ってみる
>>> import target
>>> base = target.BasePacket()
root [0] TARGET::BasePacket* base = new TARGET::BasePacket()

動作試験環境

OS X
Linux
  • Scientific Linux 6.3
  • GCC 4.4.6
  • Python 2.6.6 (yumpython-devel を入れる必要あり)
  • ROOT 5.34/04
  • SWIG 1.3.40 (yum)
  • CMake 2.8.10.2 (yum で導入可能な 2.6.4 では、ROOT の build でこけるので、2.8 以上を自分で導入する必要あり)

ROOT と CMake 以外は全て yum で取ってこられる最新のもの。

ただし、Scientific Linux の環境では /usr/include/python2.6/Python.h を見つけてくれなかったため、次のように実行しました。CMake はこれくらい自動で見つけてほしいですが…。

$ cmake ~/target -DPYTHON=ON -DPYTHON_INCLUDE_DIRS=/usr/include/python2.6 -DROOT=ON

*1:使ったことあると思いきや、ROOT 環境にべったりで自分で Makefile 書いたりする機会は少ない

*2:Autoconf を知らないので、比較しようないのですが、Autoconf は以前勉強しかけて面倒くさそうに見えたのでやめました。