先日、古い PHP で書かれた WEB アプリケーションのアップデートが可能かどうか調査していた。

その WEB アプリケーションは、Windows Server で動いており、ある .NET Framework で作られたライブラリを呼び出していた。

ライブラリの開発元に、PHP から呼べるのか問い合わせたところ、サポートしていない、という回答だった。現行バージョンでは呼び出せているので、どのようになっているか調べたところ、間に中継するクラスライブラリがはさまっていた。

PHP から .NET Framework ライブラリを呼び出すには dotnet クラスを使えば良いが、制限がある

dotnet クラスの使い方は、公式ドキュメントのdotnet クラスに例がある。

ただ、このクラスを使うためには、GAC(グローバル アセンブリ キャッシュ) にライブラリを登録しておく必要があるようだ。

GAC に登録するには、簡単な方法としては Visual Studio の developer console から gacutil コマンドを使って登録する。(PowerShell を使う方法もあり)

gacuitil /i パス

gacutil では、アセンブリ名を確認することができる。

gacutil /l

dotnet クラスのコンストラクタの $assembly_name にここで表示されるアセンブリ名を 、$datatype_name にライブラリ内のクラス名をセットしてインスタンスを作れば、この PHP のインスタンスは .NET Framework で作られたライブラリのメソッドを呼び出すことができる。

しかし、ドキュメントにもあるように、「staticクラス をインスタンス化したり、staticメソッド を呼び出すことはサポートされていません。」という制限事項がある。

私が調査したライブラリは、スタティックなファクトリメソッドでインスタンスを生成するようになっていたためか、ファクトリメソッドを呼び出すことができなかった。そのため、PHP から直接 .NET Fframework で作られたライブラリを操作することができなかった。

しかし、static メソッド呼出しができない問題を回避するために、インスタンスを生成し、メソッドを再呼出するような .NET Framework ライブラリを作れば呼び出すことはできるようだ。

C# で .NET Framework でライブラリを作って PHP から呼び出してみる

とりあえず、C# で確認してみることにした。

実装

  1. Visual Studio を起動
  2. 新しいプロジェクトの作成
  3. 「C# クラスライブラリ(.dll)を作成するためのプロジェクトです」を選ぶ
  4. 適当な名前を入れる。.NET Framework 3.5 を選択。(古い PHP-7.4 では、Framework 4.0 以上がサポートされていなかったため。今は、新しいバージョンの .NET Framework にも対応している模様)
  5. Class1.cs に以下のコードを貼り付ける。

Class1.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;

namespace PHPTestCS
{
    [ComVisible(true)]
    public class PHPTestClass
    {
        private int num;

        public PHPTestClass()
        {
            num = 2;
        }

        public int PHPTest(int v) {
            return v * 2;
        }
    }

    [ComVisible(true)]
    public class PHPTestStatic
    {
        public static PHPTestClass PHPTestFactory()
        {
            return new PHPTestClass();
        }

        public static int PHPTestInt(int v)
        {
            return v + 1;
        }

        public int PHPTestInt2(int v)
        {
            return v + 2;
        }
    }

    [ComVisible(true)]
    public class PHPTestClass2
    {
        private PHPTestClass test;
        public PHPTestClass2() {
            test = PHPTestStatic.PHPTestFactory();
        }

        public int PHPTest(int v) {
            return test.PHPTest(v);
        }
    }

}

[ComVisible(true)] を付けておかないと、PHP から見えないようだ。公式ドキュメントにもそれらしきことが書いてある。

ビルド

署名に設定しておかないと、gacutil を実行した時に、「キャッシュにアセンブリを追加しているときにエラーが発生しました: 厳密な名前のないアセンブリーをインストールしようとしました」と表示される。

  1. プロジェクトを右クリックしてプロパティを選択
  2. 署名を選択
  3. アセンブリに署名する
  4. 厳密な名前のキーファイルを選択してください->新規作成
  5. キーファイルには適当な名前を指定。
  6. Developer Console を管理者で起動し、gacutil /i dllパス
  7. gacutil /l でアセンブリ名を確認

実行、結果など

PHP.ini に以下の設定をする。

extension_dir = "ext"
extension = com_dotnet

以下、テスト用のコード。 PublicKeyToken は、キーファイルによって変わるだろうから、gacutil /l で表示されるものをセットすること。

dotnettest.php

<?php

//$cls = new DOTNET('PHPTestCS, Version=1.0.0.0, Culture=neutral, PublicKeyToken=1721851db4864432, processorArchitecture=MSIL', 'PHPTestCS.PHPTestClass');
$cls = new DOTNET('PHPTestCS, Version=1.0.0.0, Culture=neutral, PublicKeyToken=1721851db4864432', 'PHPTestCS.PHPTestClass');
echo $cls->PHPTest(100);

$cls2 = new DOTNET('PHPTestCS, Version=1.0.0.0, Culture=neutral, PublicKeyToken=1721851db4864432', 'PHPTestCS.PHPTestStatic');
// $cls2_ = $cls2->PHPTestFactory(); // -> undefined method
// echo $cls2->PHPTestInt(100); // -> undefined method
echo $cls2->PHPTestInt2(100); // -> OK

$cls3 = new DOTNET('PHPTestCS, Version=1.0.0.0, Culture=neutral, PublicKeyToken=1721851db4864432', 'PHPTestCS.PHPTestClass2');
echo $cls3->PHPTest(300);
  • スタティックメソッドは呼び出せなかった。($cls2)
  • スタティックメソッドでも、中継するクラスを作れば呼び出すことは不可能ではない。($cls3)

Visual Basic の場合

同様にして Visual Basic でもクラスライブラリを作って、PHP から呼び出せる。

Class1.vb

Imports System.Runtime.CompilerServices
Imports System.Runtime.InteropServices

Namespace PHPVBTestNS
    <ComVisible(True)>
    Public Class PHPVBTestClass
        Public Function Test(v) As Integer
            Return v * 2
        End Function
    End Class
End Namespace

ビルドしてできた dll が PHPTest.dll だったとしよう。

この場合、以下のコードからアクセスできた。(DOTNET の第一引数は、gacutil /l で表示されるものを使うこと)

PHPコード

<?php

$cls = new DOTNET('PHPTest, Version=1.0.0.0, Culture=neutral, PublicKeyToken=6c3b0adfcd58f899, processorArchitecture=MSIL', 'PHPTest.PHPVBTestNS.PHPVBTestClass');
echo $cls->Test(100);

C# の場合とクラス名が少し違っていた。「アセンブリ名.名前空間名.クラス名」のようになっているのだろうか。

Visual Basic なら使える、慣れている、という場合もあるだろう。そういった方は、Visual Basic でラッパー(被せ)を作れば良いのではないだろうか。

おわりに

調査はしたものの、仕様がお客様の要望に沿っておらず、失注した。正直悲しい。。。

折角調べたので、せめて、WEB トラフィックの増加につながるよう、公開しておきたいと思う。

個人的には、C#、.NET はデスクトップから WEB、スマートフォンまでカバーしていて、クロスプラットフォーム(Mac/Linux にも対応している?)なので、実用的だと思う。