C#で書いたライブラリをPHPやGoから呼び出す

本記事は [サムザップ Advent Calendar 2022](https://qiita.com/advent-calendar/2022/sumzap) の12/9の記事です。

.NET 7より、ソースコードを中間言語（CIL）ではなく、ネイティブコードへコンパイルするNative AOTがサポートされました。
そのNative AOTに関連して、.NET系言語**以外**からも呼び出し可能な[ネイティブライブラリが作成できる](https://learn.microsoft.com/ja-jp/dotnet/core/deploying/native-aot/#build-native-libraries)ようになっています。
他言語から.NETの、例えばC#で書いたライブラリを利用できるというのは魅力的ですので、早速試してみました。

## C#でネイティブライブラリを作成する

まずはC#のクラスライブラリプロジェクトを作成します。

```shell-session
$ dotnet new classlib -o NativeLibrary
```

作成されたプロジェクトのプロパティへ、AOTコンパイルを有効化するオプションを追加します。

```xml:NativeLibrary.csproj
<PropertyGroup>
    <PublishAot>true</PublishAot>
</PropertyGroup>
```

### 外部へ公開するメソッドを実装する
外部へ公開するメソッドには、EntryPointを指定した[UnmanagedCallersOnly属性](https://learn.microsoft.com/ja-jp/dotnet/api/system.runtime.interopservices.unmanagedcallersonlyattribute?view=net-7.0)を付ける必要があります。

```csharp:Class1.cs
using System.Runtime.InteropServices;

namespace NativeLibrary;

public class Class1
{
    [UnmanagedCallersOnly(EntryPoint = "Multiply")]
    public static int Multiply(int a, int b) => a * b;
}
```

EntryPointに指定した名前がそのまま外部から呼び出す際の関数名になります。
また、公開するメソッドにはいくつかの制約があるので、これに違反しないよう注意しましょう。

<details><summary><strong>外部へ公開するメソッドの制約</strong></summary>

- staticメソッドでなければならない
- マネージドコードから直接呼び出してはならない
- 引数はすべてBlittable型でなければならない
- ジェネリックメソッドまたはジェネリッククラスのメンバーではない
- 例外を使用してはならない

</details>

### ネイテイブライブラリをビルドする
ビルドを行うプラットフォームによって必要なパッケージは異なりますので、[公式のドキュメント](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/#prerequisites)を参照して下さい。  
例えばUbuntuではclangとzlib1g-devの導入が必要になります。

```shell-session
$ apt-get install clang zlib1g-dev
```

ライブラリのビルドは `dotnet publish` でターゲットプラットフォームの[RuntimeIdentifier](https://learn.microsoft.com/ja-jp/dotnet/core/rid-catalog#linux-rids)を指定して実行します。

```shell-session
$ dotnet publish -c Release -r linux-x64
MSBuild version 17.4.0+18d5aef85 for .NET
  Determining projects to restore...
  All projects are up-to-date for restore.
  NativeLibrary -> bin/Release/net7.0/linux-x64/NativeLibrary.dll
  Generating native code
  NativeLibrary -> bin/Release/net7.0/linux-x64/publish/
```

同時にビルドされるマネージドライブラリ（`NativeLibrary.dll`）と比較すると、ファイルのフォーマットが異なっていることが確認できます。

```shell-session
$ file publish/NativeLibrary.so NativeLibrary.dll 
publish/NativeLibrary.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=aa46182d1ecb6ad01736c7c429525947e82dccee, with debug_info, not stripped
NativeLibrary.dll:        PE32+ executable (DLL) (console) x86-64 Mono/.Net assembly, for MS Windows
```

最後に、ビルドしたネイティブライブラリを/usr/local/lib配下にコピーして、`ldconfig`でキャッシュを更新します。

```shell-session
$ cp bin/Release/net7.0/linux-x64/publish/NativeLibrary.so /usr/local/lib/libNativeLibrary.so
$ ldconfig
```

## PHPからライブラリを呼び出す
PHPからネイティブライブラリを呼び出すには[FFI](https://www.php.net/manual/ja/book.ffi)が使用できますが、
標準では有効化されていない機能であるため、必要なライブラリを導入した上で自前でPHPをビルドする必要があります。

<details><summary>FFIを有効化したPHPをビルドする</summary>

この記事の本旨とは少し外れるため、PHPのビルドは要点に絞って記載します。  
PHPのソースコードからのビルドと、FFIの有効化は下記の記事を参考にさせて頂きました。

- [ソースコード（php-src）からPHPをビルドする流れと仕組みを手を動かしながら理解する](https://zenn.dev/hgsgtk/articles/fb9af8bfc64ec37bce2d)
- [PHP7.4のFFIについて / About PHP-FFI - Speaker Deck](https://speakerdeck.com/hanhan1978/about-php-ffi?slide=9)

**libffiの導入**
```shell-session
$ apt-get install libffi-dev
```

**PHPのビルド**
```shell-session
$ cd php-src
$ ./configure --with-ffi && make all && make install
```

FFIが有効化できたかどうか`phpinfo();`を呼び出して確認します。

```shell-session
$ php -r "phpinfo();" | grep ffi
Configure Command =>  './configure'  '--with-ffi'
ffi.enable => preload => preload
ffi.preload => no value => no value
```

ffiのオプションが表示されていれば成功です。

</details>

main.phpファイルを作成して、ネイティブライブラリを呼び出すコードを実装します。

```php:main.php
<?php

$lib = FFI::cdef("int Multiply(int a, int b);", "libNativeLibrary.so");
$result = $lib->Multiply(4, 2);
echo "4 x 2 = " . $result . PHP_EOL;
```

main.phpを実行します。

```shell-session
$ php main.php
4 x 2 = 8
```

想定通りの結果が出力されました、正常にC#で記述したロジックが呼び出せているようです。

## Goからライブラリを呼び出す

main.goファイルを作成し、同様にネイティブライブラリを呼び出すコードを実装します。

```go:main.go
package main

/*
#cgo LDFLAGS: -lNativeLibrary -shared
int Multiply(int a, int b);
*/
import "C"
import "fmt"

func main() {
	result := C.Multiply(4, 2)
	fmt.Printf("4 x 2 = %d\n", result)
}

```

ビルドして実行します。

```shell-session
$ go build main.go
$ ./main
4 x 2 = 8
```

GoからもC#のロジックが呼び出せていることが確認できました。

## まとめ

.NET 7でサポートされたNative AOTにより、他言語から呼び出せるライブラリが簡単に作成できるようになりました。
実際に運用するにはいくつかハードル（公開する関数の定義の共有方法や、[AOT自体の制限](https://learn.microsoft.com/ja-jp/dotnet/core/deploying/native-aot/#limitations-of-native-aot-deployment)など）が考えられますが、  
ネイティブライブラリによって今まで以上に.NETの利用シーンが広がりそうです。


## 環境
本記事は下記環境の下作成しました。

```shell-session
$ head -1 /etc/os-release 
PRETTY_NAME="Debian GNU/Linux 11 (bullseye)"
$ dotnet --version
7.0.100
$ php --version
PHP 8.1.13 (cli) (built: Dec  7 2022  9:59:25) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.1.13, Copyright (c) Zend Technologies
$ go version
go version go1.19.4 linux/amd64
```

## 参考
1. [Native AOT deployment overview - .NET | Microsoft Learn](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/)
1. [PHP: FFI - Manual](https://www.php.net/manual/ja/book.ffi.php)
1. [ソースコード（php-src）からPHPをビルドする流れと仕組みを手を動かしながら理解する](https://zenn.dev/hgsgtk/articles/fb9af8bfc64ec37bce2d)
1. [PHP7.4のFFIについて / About PHP-FFI - Speaker Deck](https://speakerdeck.com/hanhan1978/about-php-ffi?slide=9)
1. [cgo command - cmd/cgo - Go Packages](https://pkg.go.dev/cmd/cgo)