Python for .NET の「Python for .NET」のほう、「.NET for Python」でなくて (地獄への落ち方判明)

これの続き。

前回の「ほぼ結論」:

で、残るは Python.Runtime.dll なのだが…、やはりアンマネージドな DLL とは違うのね、アッセンブリを見つける仕組みは。「GAC に置かず、なおかつ自分と同じ場所に置かない」という場合のアッセンブリの解決方法が見つからない。自分の EXE や DLL と同じ名前の設定ファイル(今の場合「PyFromCSExam.exe.config」)を書くと一定の制御が出来ることは発見したものの、例えば <probing> は目的のものに近いのに、「あんたの EXE や DLL のサブフォルダなら許してやるぜっ」てものなので、今やりたい「全然無関係の場所にある2つ」を結びつける手段にはならない。

GAC にないものだからねぇ。「コピーすべし」が唯一の解なのかもしらんなぁ。(も少し粘って調べてみるけれど、たぶんこれが答えなんじゃないかと今のところ思ってる。)

予想通りほとんど正しい。なぜなら「確実に想定した組み合わせで動いて欲しい」と願うべきだからだ、普通は。「DLL Hell」。

とはいえ、「サードパーティライブラリのバージョンアップで何か致命的バグが直ったら追従したい」といったニーズも普通は考えるし、ワタシのように、「絶賛開発中とか自分しか使わないものでまで厳格に規則に従うなんてヤダヤダヤダ」のためにも、地獄への落ち方を知っておくことは必要だ。

こんな検索で一撃:

You can invoke DLL Hell by writing an event handler for the AppDomain.CurrentDomain.AssemblyResolve event. Assign it in the Main() method.

それな。

書き方は CodeProject からすぐに見つかった

つーわけで概ねこんな具合な:

  1 # -*- coding: utf-8 -*-
  2 # ---------------------------------------------------
  3 from __future__ import absolute_import
  4 from __future__ import unicode_literals
  5 from __future__ import print_function
  6 
  7 
  8 if __name__ == '__main__':
  9     # ファイルがバラけてると実験が鬱陶しいので C# コードをここに書いてしまって
 10     # ビルドも全部やってしまう。
 11     import subprocess, os, io, sys
 12 
 13 
 14     #     site-packages にある Python.Runtime.dll
 15     import site
 16     cands = site.getsitepackages()
 17     for canddir in cands[1:]:
 18         pythonengine_dll = os.path.join(canddir, "Python.Runtime.dll")
 19         if os.path.exists(pythonengine_dll):
 20             break
 21 
 22     # C# コード
 23     #   公式サイト説明に欠けてるのは:
 24     #       using Python.Runtime;
 25     #   PythonEngine.Initialize(); がいるのかいらないのか、入ってなくても
 26     #   動いた。CPython の C API のノリなら必要なのだが?
 27     #
 28     io.open("PyFromCSExam.cs", "w").write("""
 29 using System;  // for Console, etc.
 30 using System.Collections.Generic;  // for List
 31 using System.Reflection;  // for Assembly
 32 
 33 using Python.Runtime;
 34 
 35 namespace PyFromCSExam
 36 {
 37     public class Exam
 38     {
 39         static Exam()
 40         {
 41             AppDomain.CurrentDomain.AssemblyResolve += ResolveAssembly;
 42         }
 43         static void Main(string[] args)
 44         {
 45             using (Py.GIL())
 46             {
 47                 dynamic np = Py.Import("numpy");
 48 
 49                 Console.WriteLine(np.cos(np.pi * 2));
 50 
 51                 dynamic sin = np.sin;
 52                 Console.WriteLine(sin(5));
 53 
 54                 double c = np.cos(5) + sin(5);
 55                 Console.WriteLine(c);
 56             }
 57         }
 58         static Assembly ResolveAssembly(object sender, ResolveEventArgs args)
 59         {
 60             return Assembly.LoadFile("%s");
 61         }
 62     }
 63 }
 64 """ % (pythonengine_dll.replace(os.path.sep, "/")))
 65 
 66     # ビルド
 67 
 68     #    pythonXX.dll の場所を PATH に追加しちまえよ
 69     from distutils import sysconfig
 70     pydlldir = sysconfig.get_config_var("BINDIR")  # unix系では LIBDIR のヤツ。
 71     if sys.version_info[0] == 2:
 72         def _fixenv(s):
 73             return s.encode()
 74     else:
 75         def _fixenv(s):
 76             return s
 77     os.environ["PATH"] = _fixenv(";".join([
 78         pydlldir]) + ";" + os.environ.get("PATH", ""))
 79 
 80     #    ビルドしちゃいなよ
 81     subprocess.check_call(
 82         [
 83             # csc がない、と言われる場合で「ほんとうは持ってる」場合は、
 84             # BuildTools 2007 なら、スタートメニューから「x64_x86 Cross Tools
 85             # Command Prompt for VS 2017」(等)を探して起動し、その環境から。
 86             "csc.exe",
 87             "-nologo",
 88             "-target:exe",  # as console app
 89             "-out:PyFromCSExam.exe",
 90             #"-reference:" + os.path.basename(pythonengine_dll),
 91             "-reference:" + pythonengine_dll,
 92             "PyFromCSExam.cs",
 93             ]
 94         )
 95     # 実行しちゃいなよ
 96     subprocess.check_call(
 97         [
 98             "PyFromCSExam.exe",
 99             ],
100         env=os.environ)

今回のは Python.Runtime.dll をカレントにコピーしないので、ビルド時の「-reference:」に Python.Runtime.dll へのフルパスを渡している。

ちと C# 部分が読みにくいかもしらんので、抽出するとこうしてる:

無論「c:/Python35」はワタシ固有の環境。
 1 using System;  // for Console, etc.
 2 using System.Collections.Generic;  // for List
 3 using System.Reflection;  // for Assembly
 4 
 5 using Python.Runtime;
 6 
 7 namespace PyFromCSExam
 8 {
 9     public class Exam
10     {
11         static Exam()
12         {
13             AppDomain.CurrentDomain.AssemblyResolve += ResolveAssembly;
14         }
15         static void Main(string[] args)
16         {
17             using (Py.GIL())
18             {
19                 dynamic np = Py.Import("numpy");
20 
21                 Console.WriteLine(np.cos(np.pi * 2));
22 
23                 dynamic sin = np.sin;
24                 Console.WriteLine(sin(5));
25 
26                 double c = np.cos(5) + sin(5);
27                 Console.WriteLine(c);
28             }
29         }
30         static Assembly ResolveAssembly(object sender, ResolveEventArgs args)
31         {
32             return Assembly.LoadFile("C:/Python35/lib/site-packages/Python.Runtime.dll");
33         }
34     }
35 }

まぁなんというか、「開発時だけそうしたい」とかなら #ifdef みたいな条件付コンパイルでも仕掛けときたい気はするわね。確か C# はそれ、出来たよな?