.NET (C#)からpythonを呼び出す

# .NET (C#)からpythonを呼び出す

[pythonnet](https://github.com/pythonnet/pythonnet)を使って、.NETからpython(cpython)コードを呼び出す方法。
例えば、「Chainerで作ったAI処理を、.NETでつくったGUIから叩きたい！」とかいうときに有用。

python -> .NET(C#)の資料はたくさんあるのに、.NET -> pythonの資料がほとんどない[^1]ので作成。

[^1]: ※[動かなかった…という記事](https://qiita.com/y-tsutsu/items/0c4561d6478be32e6f8e)はあったのでそっとリンク。

## 前提

この記事は以下の知識があることを前提に作成した。

*   C#がある程度わかること。
*   pythonがある程度(pip等の環境管理の仕組みも含め)わかること。

## 手順

作業目標は、「Windows(x64)環境で、VS2017を使い、C#からpython3.7(x64)のコードを呼び出すサンプルを作る」とする。
なお、pythonnetは、本記事執筆時点(2019/07)で、python 3.7まで対応している。

### pythonnetの設定

pythonnetをビルドするための設定を行う。
※nugetにビルド済のパッケージがあるが、あまり更新されていないようなので使用しない。

[pythonnetのgithubリポジトリ](https://github.com/pythonnet/pythonnet)から最新コードをcloneし、pythonnet.slnを開く。
VS2015の場合はpythonnet.15.slnを使う。vs2019以降を使う場合は適当に変換してがんばる(未確認)。

ソリューションのビルド構成がいろいろあるので、適切なものを選ぶ。
構成名は、`(Debug|Release)(Mono|Win)(|PY3)`という構造になっている。
※ただし、[この設定を廃止し、実行時に動的に選択可能にする改善](https://github.com/pythonnet/pythonnet/issues/910)が検討されているようなので、このあたりの手順は、そのうち大きく変わるかも。

1. `Debug|Release`
  おなじみの、Debug/Releaseビルドの選択。

1. `Mono|Win`
  Mono(Linux)用か通常のWindows用か。

1. `|PY3`
  なにも付かないほうはpython2系向け、PY3はpython3系向けにビルド。

今回はWindows環境でpython3.7を呼び出すのが目的なので、`ReleaseWinPY3`を選ぶ。
なお、プラットフォーム(x86/x64)はどちらでもよい(今回ビルドしたい`Python.Runtime`プロジェクトはAnyCPU設定になっているため)。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426308/d78636af-c73c-1dd9-5457-c424bbcee92b.png)

選んだら、プロジェクト`Python.Runtime`のプロジェクト プロパティを開き、コンパイルシンボルを確認する。
`PYTHON3;PYTHON37;UCS2`のような文字列が定義されているので、狙ったpythonバージョンになっているか確認する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426308/84a0d647-f202-0747-71c9-c9b782fb1427.png)

今回はpython3.7を動かすのでPYTHON37でよいが、python3.5にしたい場合はPYTHON35のように書き換える。
有効な設定値は、`src/runtime/runtime.cs`の#if節を参照するとわかる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426308/d8d762b0-5f98-3b87-0862-42be9bc91ab5.png)

### pythonnetのビルド

設定を済ませたプロジェクトをビルドする。

`src/runtime/bin`以下に`Python.Runtime.dll`が出力されるので、回収しておく。

同時に、`Python.Runtime.pdb`(デバッグ情報)と`Python.Runtime.xml`(ドキュメントコメントの情報)も回収しておくと、あとで少し幸せになれるかも。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426308/48d37046-a52f-68e8-27b9-384dff420598.png)

### 呼び出し元プロジェクトの作成

pythonコードの呼び出し元となるプロジェクト / ソリューションを作る。

サンプルでは、.NET Framework 4.6.1向けのC#コンソール アプリとした。

※pythonnetは.NET Framework 4をターゲットに作られているので、それを呼び出せる設定にすること。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426308/b09b1a2d-c1c4-e670-f4d2-2f390c4a4d9a.png)

### プラットフォームの変更

64bit用のPythonのDLLを呼び出すには、呼び出し元のexeも64bit向けにビルドされている必要があるため、その設定を行う。

作成したソリューションの構成マネージャーを開く。

プロジェクトのプラットフォームがAnyCPUとなっているので、x64を新規作成する。
※このとき、"新しいソリューション プラットフォームを作成する"にチェックを入れておくこと。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426308/f3ed86c5-3423-7a00-5950-26106d0a10eb.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426308/f0dd8296-6bfe-f119-ae6a-434d0c360615.png)

作成したら、プロジェクト プラットフォームとソリューション プラットフォームの両方から、AnyCPU設定を削除しておく。
※残しておくと、うっかりAnyCPU設定を使ってしまってドハマリする場合があるため。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426308/b1a3c6dc-d03b-9e0e-ffd1-a6189fe40e98.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426308/9cc31488-10d4-26e2-7cce-4bcb30916e06.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426308/86c3b1a2-4ee6-a6e9-9458-1b0288845dbb.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426308/739f4e6c-f2d3-a149-082c-017eadf031d4.png)


### 参照設定

pythonnetのビルド時に回収しておいたDLLを、作成したプロジェクトのディレクトリにコピーする。

プロジェクトの参照設定から、コピーしたDLLに参照を設定する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426308/e3bae8bb-3477-d50b-8b9c-0cb4de2a2ceb.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426308/8b070268-cf8f-8e5a-e381-c84fe870b159.png)
※見逃しがちだが、右下に参照ボタンがある。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426308/f67b4b94-1fce-541d-c140-1e4394f09ff5.png)
追加されるとこうなる。

### 実装

以下を実装する。

```cs
using Python.Runtime;
using System;
using System.IO;
using System.Linq;

namespace PythonTest
{
    class Program
    {
        /// <summary>
        /// プロセスの環境変数PATHに、指定されたディレクトリを追加する(パスを通す)。
        /// </summary>
        /// <param name="paths">PATHに追加するディレクトリ。</param>
        public static void AddEnvPath(params string[] paths)
        {
            var envPaths = Environment.GetEnvironmentVariable("PATH").Split(Path.PathSeparator).ToList();
            foreach (var path in paths)
            {
                if (path.Length > 0 && !envPaths.Contains(path))
                {
                    envPaths.Insert(0, path);
                }
            }
            Environment.SetEnvironmentVariable("PATH", string.Join(Path.PathSeparator.ToString(), envPaths), EnvironmentVariableTarget.Process);
        }

        /// <summary>
        /// プログラムのエントリポイント。
        /// </summary>
        /// <param name="args">コマンドライン引数。</param>
        static void Main(string[] args)
        {
            // *-------------------------------------------------------*
            // * python環境の設定
            // *-------------------------------------------------------*

            // python環境にパスを通す
            // TODO: 環境に合わせてパスを直すこと
            var PYTHON_HOME = Environment.ExpandEnvironmentVariables(@"%userprofile%\Anaconda3\envs\pythonnet_test");

            // pythonnetが、python本体のDLLおよび依存DLLを見つけられるようにする
            AddEnvPath(
              PYTHON_HOME,
              Path.Combine(PYTHON_HOME, @"Library\bin")
            );

            // python環境に、PYTHON_HOME(標準pythonライブラリの場所)を設定
            PythonEngine.PythonHome = PYTHON_HOME;

            // python環境に、PYTHON_PATH(モジュールファイルのデフォルトの検索パス)を設定
            PythonEngine.PythonPath = string.Join(
              Path.PathSeparator.ToString(),
              new string[] {
                  PythonEngine.PythonPath,// 元の設定を残す
                  Path.Combine(PYTHON_HOME, @"Lib\site-packages"), //pipで入れたパッケージはここに入る
                  Path.Combine(@"C:\foo\bar\my_packages"), //自分で作った(動かしたい)pythonプログラムの置き場所も追加
              }
            );

            // 初期化 (明示的に呼ばなくても内部で自動実行されるようだが、一応呼ぶ)
            PythonEngine.Initialize();

            // *-------------------------------------------------------*
            // * pythonコードの実行
            // *-------------------------------------------------------*
            // Global Interpreter Lockを取得
            using (Py.GIL())
            {
                // モジュールの探索パスを表示 (pythonコードを直接指定して実行)
                PythonEngine.RunSimpleString(@"
import sys
import pprint
print('module path =')
pprint.pprint(sys.path)
");

                // numpyのオブジェクトを取得し、呼び出してみる
                dynamic np = Py.Import("numpy");
                Console.WriteLine($"np.cos(np.pi * 2) =  {np.cos(np.pi * 2)}");

                // 自作コードも叩ける
                dynamic myMath = Py.Import("my_awesome_lib.my_math"); // "from my_awesome_lib import my_math"
                dynamic calculator = myMath.Calculator(5, 7); // クラスのインスタンスを生成
                Console.WriteLine($"5 + 7 = {calculator.add()}"); // クラスのメソッド呼び出し
                Console.WriteLine($"sum(1,2,3,4,5) = {myMath.Calculator.sum(new[] { 1, 2, 3, 4, 5 })}"); //staticメソッドも当然呼べる
                dynamic dict = myMath.GetDict(); // 辞書型を返す関数呼び出し
                Console.WriteLine(dict[3]); // 辞書からキーを指定して読み取り
            }

            // python環境を破棄
            PythonEngine.Shutdown();

            Console.WriteLine("Press any key to continue...");
            Console.ReadKey();
        }
    }
}
```

ただし、**このままではまだ動かない**。

## python環境を作る

python環境を導入する。

今回は、[Anaconda](https://www.anaconda.com/)で、"pythonnet_test"という名前のpython3.7環境(64bit)を作り、利用する。
よって、python環境のルートディレクトリは`%userprofile%\anaconda\envs\pythonnet_test`となる(環境によって異なる可能性があるので適宜調整のこと)。

また、C#からpythonの追加パッケージが利用できることを確認するため、numpyを追加インストールした。

python環境を別の方法で導入する場合は、ルートディレクトリのパスを確認し、numpyもインストールしておくこと。

## 自前パッケージを配置

自作pythonコードが呼び出せることを確認するため、呼び出し先となるpythonプログラムを準備する。

以下コードを、<任意のディレクトリ>\my_awesome_lib\my_math.pyに配置する。

※配置したディレクトリは後で使うので覚えておくこと。サンプルでは`C:\foo\bar\my_packages\my_awesome_lib\my_math.py`に置いた。

```python
# よくありがちなしょうもないテストコード
class Calculator:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def add(self):
        return self.x + self.y

    @staticmethod
    def sum(num_list):
        return sum(num_list)

def GetDict():
    return {
        0 : "this",
        1 : "is",
        2 : "my",
        3 : "dictionary",
        }
```

## python環境にパスを通す

作成したpython環境にパスを通す。これが一番難しい(というか情報がない)。

ソースコード中、"python環境の設定"というコメント以降がこの設定に相当するので、コードを修正する。

### pythonnetがpython本体のDLLを見つけられるようにする

pythonnetがpython本体のDLLを見つけられるよう、環境変数(PATH)にディレクトリを追加する。

サンプルでは、ユーティリティ関数`AddEnvPath()`によって、以下を設定している：

*   python環境のルート

    pythonXX.dll(python3.7ならpython37.dll)のあるディレクトリ。

*   python環境のルート\Library\bin

    このディレクトリに、pythonXX.dllが依存しているDLLの一部が格納されていた。
    python環境の作り方によっては必要ないか、別のディレクトリを指定する必要があるかも。


パスの通し方には複数の方法があるが、この例と同様、プログラム中(コード)で設定することをお勧めする。

*   プログラム中(コード)で設定

    きちんと設定されていることを保証できる。おすすめ。
    ただし、サンプルではパスをハードコードしているが、普通は設定ファイルとかから読むようにすべき。

*   Windowsのシステム環境変数に設定

    システム全体に効くため楽だが、複数のpython環境が運用できなくなる。

*   exe起動前に(bat等で)事前設定する

    デバッグ実行などがやりにくい。

### python本体が、pythonのモジュール(標準モジュールを含む)を見つけられるようにする

python本体に、pythonのモジュールを探索するパスを設定する。

これが正しく設定されていないと、pythonの標準ライブラリやpip等で入れたライブラリ、自作コードを見つけることができない。

設定すべき項目は2種類ある。

*   PYTHON_HOME

    python環境のルート。

*   PYTHON_PATH

    pythonモジュールの探索場所。セミコロンで区切って指定する。

PYTHON_PATHには、デフォルトで以下が設定されている：

*   %PYTHON_HOME%\pythonXX.zip
*   %PYTHON_HOME%\DLLs
*   %PYTHON_HOME%\lib
*   カレントディレクトリ  

サンプルでは、これらに加え、以下を設定している。

*   pipのインストール先ディレクトリ (Lib\site-packages)
*   自作コードの存在するディレクトリ (～\my_packages、実際に置いた場所に合わせて適宜修正すること)

必要なら、さらに追加することもできる。

## 実行

ここまで設定すると、サンプルが動くようになる(はず)。

ビルドして実行してみて、以下のように出力されれば成功。

```console
module path =
['C:\\～～～',
 ...
 'C:\\Windows\\Microsoft.NET\\Framework64\\v4.0.30319\\']
np.cos(np.pi * 2) =  1.0
5 + 7 = 12
sum(1,2,3,4,5) = 15
dictionary
Press any key to continue...
```

Pythonの関数やクラスが呼び出せ、値をやり取りできていることが分かる。

## ハマりどころ(基本編)

よくあるトラブル。

### `Python.Runtime.PythonException: ModuleNotFoundError : No module named '～～'`が起きる

pythonモジュールを見つけられていない。おそらく、PythonPathの設定をミスしている。

見つからないと怒られたモジュールの実体(ファイル)がどこにあるか調べ、PythonPathが適切に設定されているか確認する。

### `BadImageFormatException`が起きる

DLL関連の問題は、だいたい何でもこの例外になる。

原因が多岐にわたるので調査が難しいが、以下のどれかが原因のことが多い。

*   pythonのDLLがあるディレクトリにパスが通っていない

*   pythonnetのビルド時に、適切なpythonバージョンを指定しなかった
    python3.5の環境で動かしたいのにpython3.7向けにビルドしてしまった、など。
    pythonnetが探しに行くDLL名はビルド時に決まるので(python3.7向けにビルドした場合はpython37.dllを探しに行く)、ビルド時の環境と実環境が不一致だとDLLの読み込みに失敗する。

* pythonのDLLが依存しているDLLにパスが通っていない(見つからない)
    DLLが見つかっても、そのDLLが依存しているDLLが見つからない場合は、やはり読み込みエラーとなる。

* 呼び出し元プロジェクト・python本体のプラットフォーム(x86/x64)が一致してない
    特に、 呼び出し元のプロジェクトをAnyCPU設定にするのは混乱の元になるので避けたほうがよい(もともとの挙動がややこしい上、.NET 4.5以前と以後での挙動が異なる)。
    ※pythonnet本体は、AnyCPUでビルドされるため、呼び出し元プロジェクトの設定に従う。

この手の問題が発生してしまった場合は、以下のような調査手段で何とかする。

*   [Dependencies](https://github.com/lucasg/Dependencies)でDLLの依存関係を追っかける

*   dumpbinコマンドで各DLLのプラットフォーム(x86/64)を確認する
    e.g. `dumpbin /headers hoge.dll`

*   [Process Monitor](https://docs.microsoft.com/ja-jp/sysinternals/downloads/procmon)でロードに失敗したDLLを特定する
    IRP_MJ_CREATEが何度も失敗しており(DLLの探索先パスを全て探すため)、最後までLoad Imageが実行されていない(= DLLが見つからなかった)DLLが犯人。
    また、Load Imageした結果がSUCCESSでないDLLも怪しい。
  フィルタ設定を以下にするとわかりやすい。
    *   Process：python呼び出し元のexe
    *   Operation：`IRP_MJ_CREATE`(ファイルのオープン)と`Load Image`(DLLの読み込み)をInclude

    なお、`IRP_MJ_CREATE`はFiler -> Enable advanced outputを有効にしていないと表示されない。

## ハマりどころ(応用編)

ちょっと難しいことをやろうとするとハマること。

### 複数スレッドからの実行

C#で、複数のスレッド(pythonnetを初期化した以外のスレッド)からPythonコードを呼び出す場合には、注意が必要。

Python(CPython)は、設計上、[GIL](https://ja.wikipedia.org/wiki/%E3%82%B0%E3%83%AD%E3%83%BC%E3%83%90%E3%83%AB%E3%82%A4%E3%83%B3%E3%82%BF%E3%83%97%E3%83%AA%E3%82%BF%E3%83%AD%E3%83%83%E3%82%AF)を採用している(GIL自体の説明は本題とずれるので割愛)。
また、明示的に開放処理を行わない限り、pythonnetはメインスレッド(= pythonnetを初期化したスレッド)でGILを保持したままにする。
よって、C#の別スレッドからPythonのコードを実行しようとした場合、GILの取得(using(Py.GIL())でデッドロックする。

これを回避するには、pythonnetの初期化後(PythonEngine.Initialize()の呼び出し後)に、PythonEngine.BeginAllowThreads()を呼び出し、明示的にGILを解放する。
これにより、他スレッドからもGILが取得できるようになり、C#の複数スレッドからPythonのコードが呼び出せるようになる。
ただし、GILで排他されることには変わりがないことには注意が必要。
Python側コードの並列実行はされず、単に複数スレッドから呼び出せるようになるだけ。

なお、pythonnetのBeginAllowThreads()メソッドが何をやっているのかは、[PythonのC APIのドキュメント](https://docs.python.org/ja/3/c-api/init.html#releasing-the-gil-from-extension-code)を読むとイメージがつかめる。

### 巨大データの扱い

C#からPythonに巨大な配列やリストを単純に渡すと、要素全てが個別にpythonのオブジェクトに変換されるため、とてつもなく遅くなる。
※ためしに4k解像度の画像データ(uint8の画素値)が入った配列を渡したら、なにもしないpython関数を呼び出すだけでも、秒単位の時間がかかった…

こういう場合には、ポインタ経由での引き渡しが有効。
たとえば8bit グレースケールの画像を受け渡す場合、以下のようにする。

```python
import numpy as np
import ctypes.util
from ctypes import *

def my_func(img_ptr, width, stride, height):
    # ポインタからnumpyのarrayを作る
    img = np.ctypeslib.as_array((stride * height * ctypes.c_uint8).from_address(img_ptr)).reshape(1, height, stride)
    # imgに対していろいろ処理する…
```

C#からは、以下のように呼び出す。

```cs
// この画像を引き渡したい
var hugeImage = new Bitmap(65536, 65536, PixelFormat.Format8bppIndexed);

BitmapData bitmapData = null;
try
{
    // 画像をロック
    bitmapData = hugeImage.LockBits(
       new Rectangle(new Point(0, 0), image.Size),
       ImageLockMode.ReadOnly,
       PixelFormat.Format8bppIndexed
       );

    using (Py.GIL())
    {        
        // pythonモジュールを読み込む。ファイル名等は適宜変更すること
        dynamic myPython = Py.Import("myPython");
        myPython.my_func(
            bitmapData.Scan0.ToInt64(), // 画像バッファの先頭ポインタを引き渡す
            bitmapData.Width, //当然ながら画像サイズとストライドも必要
            bitmapData.Stride,
            bitmapData.Height
            );
    }
}
finally
{
    // ロックした画像を解放
    if (bitmapData != null) hugeImage.UnlockBits(bitmapData);
}
```

画像ではなく配列やリストを渡したい場合は、[Marshalクラス](https://docs.microsoft.com/ja-jp/dotnet/api/system.runtime.interopservices.marshal)を使ってIntPtrにさえ変換してしまえば、同じように渡せる。
unsafeコードを使えるなら、[fixed](https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/keywords/fixed-statement)を使っても、もちろんよい。

## pythonnetを使わない方法

pythonnetを使わない方法としては、以下のような方法での呼び出し方が考えられる。

### python側をコマンドラインプログラムとして設計し、C#側から起動する

データのやりとりは、標準入出力やファイルを経由して行う。

pros:

*   シンプルでわかりやすい。
*   pythonランタイム環境と疎結合にできる。

cons:

*   起動終了などのオーバーヘッドが大きい。
*   細かい制御がしにくい。

### python側をサーバとして設計し、C#側からサーバに対して要求を投げる

python側をサービスにしてしまう。
WebAPIサーバにしてもよいし、[Apache Thrift](https://thrift.apache.org/)や[gRPC](https://www.grpc.io/)のようなRPCフレームワークを使ってもよい。

pros:

*   設計自由度が高い。
*   オーバーヘッドが比較的小さい(ただしプロセス間通信やシリアライゼーションは発生するので、同一プロセスで処理するよりはやや遅いはず)。
*   pythonランタイム環境と疎結合にできる。

cons:

*   プロセスが2個になり、起動終了管理などが必要になる。
*   RPC部分を別途設計・実装・メンテしないといけない。

### [Ironpython](https://ironpython.net/)を使う

cpythonと決別する。
「既存のpythonコードをC#から叩きたい」という用途には、おそらく茨の道。

pros:

*   Ironpython自体が.NET上で動くpython処理系なので、.NETとの相性がいい。

cons:

*   cpythonとの互換性に難がある(例えば、*.pydを使うライブラリはほぼ使えないと思ったほうが良い)。
*   python2.x系しかサポートしていない(一応、3.xをサポートするIronpython 3も開発中らしいが、コミットログを見る限りほとんど停滞している)。