Pythonnetを使って.NetからPythonコードを実行してみました（Hallo World編）

[こちらの記事](https://qiita.com/hogegex/items/3743225a7af13e93df78)で.NetからPythonを実行できると知り、[Pythonnet](https://github.com/pythonnet/pythonnet)を使ってみようと試してみました。
速攻でハマったので、備忘録としてメモしておきます。

## 環境
開発に用いている環境やPythonのバージョン、Pythonnetは以下の通りです。
- Windoiws 10 64bit
- Anaconda 3.8
- Visual Studio 2019
- Pythonnet 2020/7/31時点のソースコード

## 準備
まずは、[Pythonnet](https://github.com/pythonnet/pythonnet)からソースコード一式をダウンロードしてきます。
展開してpythonnet.slnがあるフォルダを開きます。

### Pythonnetのコンパイル
ソリューションファイルpythonnet.slnを開きます。

対象とするOSやPythonバージョンなどは、プロジェクトの構成から
- ReleaseWinPy3(Windows版のPython3系リリース版)
- DebugMono（Mono版のPython2系デバッグ版）
などのような形で指定できるようになっています。

これをいじると、プロジェクトの「条件付きコンパイルシンボル」の定義値が書き換わって、ソース内の`#if`による条件が切り替わる仕組みですね。

`runtime.cs`というソースコードを見てみると、以下のように切り替えられていることがわかります。

```
#if PYTHON34
        const string _minor = "4";
#elif PYTHON35
        const string _minor = "5";
#elif PYTHON36
        const string _minor = "6";
#elif PYTHON37
        const string _minor = "7";
#elif PYTHON38
        const string _minor = "8";
#else
#error You must define one of PYTHON34 to PYTHON38
#endif

#if WINDOWS
        internal const string dllBase = "python3" + _minor;
#else
        internal const string dllBase = "python3." + _minor;
#endif
```

これで、OSによるPythonDLLの名称の違いなどを切り替えているのですね。

今回の環境では、WindowsでPython3.8ですので、ReleaseWinPY3の構成でビルドします。
Python3.8を使うので、Python.Runtimeのプロパティの「ビルド」の項目の「条件付きコンパイルシンボル」の内容を、`PYTHON3;PYTHON38;UCS2`のように修正しておきましょう。

Python.RuntimeのプラットフォームはAny CPUしか選べないので、このまま。

ビルドすると、`bin\Python.Runtime.dll`ができあがるので、これを好きな場所にコピーしておきます。

## サンプルコード作成
さて、これでPythonnetライブラリの準備ができたので、簡単なHallo worldアプリを作ってみましょう。
[こちらの記事](https://qiita.com/hogegex/items/3743225a7af13e93df78)にあるサンプルを参考に作ってみます。

なお、上記の記事ではC#で書いてありますが、こちらはVB.Netで書いてます。

### プロジェクト作成
まず、Visual Studioで「新しいプロジェクトの作成」から、「コンソールアプリ（.NET Framework）」を選んでプロジェクトを作成します。
ここでは、フレームワークとして適当に.NET Framework 4.7.2を指定しました。

そして、Python.Runtime.dllを参照できるように、プロジェクトのプロパティから「参照設定」を選び、「追加」で`Python.Runtime.dll`を置いた場所を指定しましょう。

これで準備は完了です。

### Hallo Worldなサンプルコード作成

あとは以下のサンプルコードを作成。

```
Imports Python.Runtime
Imports System
Imports System.IO
Imports System.Linq

Class TestPython
    Public Sub AddEnvPath(paths As String())
        Dim envPaths As IList(Of String) = Environment.GetEnvironmentVariable("PATH").Split(Path.PathSeparator.ToString).ToList

        For Each path In paths
            If path.Length > 0 And Not envPaths.Contains(path) Then
                envPaths.Insert(0, path)
            End If
        Next
        Environment.SetEnvironmentVariable("PATH", String.Join(Path.PathSeparator.ToString, envPaths), EnvironmentVariableTarget.Process)
    End Sub
End Class

Module Module1

    Sub Main()
        Dim test_python = New TestPython

        Dim PYTHON_HOME = "C:\Users\xxx\anaconda3\envs\Python38"

        vms_python.AddEnvPath({PYTHON_HOME,
                               Path.Combine(PYTHON_HOME, "Library\bin")})

        PythonEngine.PythonHome = PYTHON_HOME

        Dim python_paths() As String = {PythonEngine.PythonPath, Path.Combine(PYTHON_HOME, "Lib\site-packages"), Path.Combine("C:\tmp")}
        PythonEngine.PythonPath = String.Join(Path.PathSeparator.ToString, python_paths)

        PythonEngine.Initialize()

        Using Py.GIL()
            PythonEngine.RunSimpleString("import sys")
            PythonEngine.RunSimpleString("print(""Hallo World"")")
        End Using

    End Sub

End Module
```

内容としては、
- 環境変数`PATH`にパスを追加するヘルパークラス`TestPython`を作成
- `PYTHON_HOME`にAnacondaのパスを記述
- `PYTHON_HOME`と`PYTHON_HOME\Library\bin`を環境変数`PATH`に追加
という準備をおこなってから、`Python.Runtime`に対して、
- `PythonEngine.PythonHome = PYTHON_HOME`でPythonがある場所を教える
- `PythonEngine.PythonPath`にPythonのサーチパスを教える
- `PythonEngine.Initialize()`で初期化
としたのち、

```
Using Py.GIL()
    PythonEngine.RunSimpleString("import sys")
    PythonEngine.RunSimpleString("print(""Hallo World"")")
End Using
```
で`Hallo World`を表示するという単純なプログラムです。

### 実行
というわけで実行してみましょう。

```
ハンドルされていない例外: System.DllNotFoundException: DLL 'python3.8' を読み込めません:指定されたモジュールが見つかりません。 (HRESULT からの例外:0x8007007E)
```
DLLが読み込めないというエラーです。

### 原因と解決方法
とりあえず、原因と解決方法だけ、さくっと書きます。

調べてみたところ、

- `python38.dll`ではなく、`python3.8.dll`を探しに行っている（Windowsでは`python38.dll`という名前）
- <del>dumpbinコマンドで調べてみると、`Python.Runtime.dll`がx86としてビルドされている</del>

<del>の二点</del>が原因だとわかりました。
(2020/08/04 コメントいただき修正。Python.Runtime.dllがAnyCPUでビルドされているのは問題ありませんでした。呼び出し側のプログラムをx64でビルドしていたつもりが、AnyCPUになっていたため、32bit呼び出しになっていたようです。呼び出し側プログラムのAnyCPUは念の為削除しておいた方が安全なようです)

よって、解決方法は以下<del>の二点</del>です。

- Pythonnetのビルド時に`WINDOWS`のシンボルが定義されていないため`python3.8.dll`という名前のDLLを探しに行っているので、Python.Runtimeの「条件付きコンパイルシンボル」に`WINDOWS`シンボルを追加する
- <del>Python.Runtimeのターゲットプラットフォームを`x64`にする</del>

こうしてビルドしたDLLを参照するようにする（一度古いDLLを除外してから再度参照させる必要あり）と、以下のように無事に実行結果が得られました。

```
Hallo World
```

なんか、Hallo Worldだけでかなり苦労したので、この記事はここまで。
実際にPythonを呼び出して使っていくコードを書くのは、次の記事にしたいと思います。