RDKitのC++ライブラリを使ってC#アプリを作成する[Windows編]

# 前回

https://qiita.com/kyusque/items/957933f1cc457011b66a

# 今回

前回はC++で直接コンソールアプリを作成しましたが、今回はラッパー関数のダイナミックライブラリを作成して別言語(C#)でコンソールアプリを作成します。

# 開発環境の構築

- Visual Studio 2019
- vcpkg
- dotnet
  - Visual Studio Installerから「.NET デスクトップ開発」を追加してください

# RDKitライブラリを使ってラッパーライブラリを作成

RDKitのスタティックライブラリを利用して、C#で利用するラッパー関数を持つダイナミックライブラリを作成します。
この際、RDKitのスタティックライブラリのビルドが終わっている必要があるので、[前回の記事](https://qiita.com/kyusque/items/957933f1cc457011b66a)の「rdkitライブラリ(static)のビルド」まで進めてください。

## フォルダ構成

RDKitWrapperとしてライブラリを作成します。
フォルダ構成は以下の通りです。
簡単のため、例のごとくCドライブ直下に作成します。

RDKitWrapper
├─ CMakeLists.txt
├─ rdkit_wrapper.cpp
└─ rdkit_wrapper.def

### CMakeLists.txt

プロジェクト名以外前回のコンソールアプリとほぼ変わりませんが、add_executableがadd_libraryになっており、ライブラリを作成する設定になっています。

```cmake:CMakeLists.txt
cmake_minimum_required(VERSION 3.22)
project(RDKitWrapper)
set(RDKit_DIR ${RDKit_ROOT_DIR})

add_library(RDKitWrapper SHARED rdkit_wrapper.cpp rdkit_wrapper.def)
find_package(Freetype REQUIRED)
find_package(RDKit REQUIRED)
target_include_directories(RDKitWrapper
    PRIVATE
    ${RDKit_INCLUDE_DIRS}
    C:/src/vcpkg/installed/x64-windows-static/include/cairo
)
target_link_directories(RDKitWrapper
    PRIVATE
    C:/rdkit/win64/lib/Release
)
target_link_libraries(RDKitWrapper
    ChemReactions
    DataStructs
    Depictor
    FileParsers
    GenericGroups
    GraphMol
    MolDraw2D
    MolTransforms
    RDGeneral
    RDGeometryLib
    RingDecomposerLib
    SmilesParse
    SubstructMatch
    coordgen
    ${FREETYPE_LIBRARIES}
)

```


### rdkit_wrapper.cpp

SMILESからSVGを出力する関数です。
外部から実行する関係上、外部から渡すバッファーにsvgを書き込む形にしており、マーシャリングも行っています。
エラーも外部から読み込むことが難しいので、標準エラー出力にメッセージを出力する形にしています。
(もし略)

```cpp:rdkit_wrapper.cpp
#include <iostream>
#include <string.h>
#include <GraphMol/SmilesParse/SmilesParse.h>
#include <GraphMol/MolOps.h>
#include <GraphMol/MolDraw2D/MolDraw2DSVG.h>
#include <GraphMol/Depictor/RDDepictor.h>
#include <GraphMol/FileParsers/MolFileStereochem.h>

extern "C"
{
    int DrawSVG(char *smiles, char *buf, size_t bufsize);
}

int DrawSVG(char *smiles, char *buf, size_t bufsize)
{
    try
    {
        auto mol = RDKit::SmilesToMol(smiles);
        if (mol == nullptr)
        {
            throw std::runtime_error("failed to parse smiles");
        }
        RDKit::MolOps::addHs(*mol);
        auto drawer = new RDKit::MolDraw2DSVG(300, 300, -1, -1, true);
        drawer->drawMolecule(*mol);
        std::string text = drawer->getDrawingText() + "</svg>\n";
        if (bufsize < text.length())
        {
            delete drawer;
            delete mol;
            std::string msg = "bufsize is not enough. ";
            msg += std::to_string(bufsize);
            msg += " < ";
            msg += std::to_string(text.length());
            throw std::runtime_error(msg);
        }
        for (size_t i = 0; i < text.length(); i++)
        {
            buf[i] = text[i];
        }
        buf[text.length()] = '\0';
        delete drawer;
        delete mol;
    }
    catch (std::runtime_error e)
    {
        std::cerr << "RDKitWrapper runtime_error: " << e.what() << std::endl;
        return -1;
    }
    catch (...)
    {
        std::cerr << "RDKitWrapper unknown exception" << std::endl;
        return -1;
    }
    return 0;
}


```

### rdkit_wrapper.def

ライブラリで外部から読み込める関数を定義します。
EXPORTS下の関数が外部から読み込めます。

```cpp:rdkit_wrapper.def
LIBRARY RDKitWrapper
EXPORTS
    DrawSVG
```

## ラッパーライブラリのプロジェクトの生成

今回はダイナミックライブラリを作りたいのでtripletはx64-windows-staticではなくx64-windowsです。

```
cd C:/RDKitWrapper
mkdir win64 ||:
cmake -S. -Bwin64 \
	-DRDKit_ROOT_DIR=C:/rdkit/win64 \
	-DCMAKE_TOOLCHAIN_FILE=C:/src/vcpkg/scripts/buildsystems/vcpkg.cmake \
	-DVCPKG_TARGET_TRIPLET=x64-windows \
	-DVCPKG_APPLOCAL_DEPS=OFF
```

## ラッパーライブラリのプロジェクトのビルド

開発者用のPowerShellを開いてビルドコマンドを実行します。
[Windowsメニュー]>[Visual Studio 2019]>[Developer PowerShell 2019]

```powershell
cd win64
msbuild ALL_BUILD.vcxproj /p:Configuration=Release
```

## ラッパーライブラリの構成の確認

### 依存性の確認

作成したライブラリに必要ない依存性が入っていないか確認します。
開発者用のPowerShellのまま以下のコマンドを実行します。
```powershell:Developer Powershell for VS 2019
dumpbin.exe /dependents .\win64\Release\RDKitWrapper.dll
```

以下のような出力なら、Windowsでデフォルトで読み込むdllなので問題ないです。
```
Dump of file .\win64\Release\RDKitWrapper.dll

File Type: DLL

  Image has the following dependencies:

    KERNEL32.dll
    MSVCP140.dll
    VCRUNTIME140.dll
    VCRUNTIME140_1.dll
    api-ms-win-crt-runtime-l1-1-0.dll
    api-ms-win-crt-heap-l1-1-0.dll
    api-ms-win-crt-stdio-l1-1-0.dll
    api-ms-win-crt-math-l1-1-0.dll
    api-ms-win-crt-locale-l1-1-0.dll
    api-ms-win-crt-string-l1-1-0.dll
    api-ms-win-crt-environment-l1-1-0.dll
    api-ms-win-crt-utility-l1-1-0.dll
    api-ms-win-crt-time-l1-1-0.dll
    api-ms-win-crt-convert-l1-1-0.dll

```

### エクスポート関数の確認

作成したライブラリで必要な関数を読み込めるか確認します。
開発者用のPowerShellのまま以下のコマンドを実行します。
```powershell:Developer Powershell for VS 2019
dumpbin.exe /exports .\win64\Release\RDKitWrapper.dll
```

以下のような出力ならDrawSVG関数が適切にエクスポートされているので問題ないです。

```
Dump of file .\win64\Release\RDKitWrapper.dll

File Type: DLL

  Section contains the following exports for RDKitWrapper.dll

    00000000 characteristics
    FFFFFFFF time date stamp
        0.00 version
           1 ordinal base
           1 number of functions
           1 number of names

    ordinal hint RVA      name

          1    0 00022640 DrawSVG
```


# C#コンソールアプリの作成

前節で作成したDLLを用いてC#のコンソールアプリを作成します。
ライブラリでSMILESからSVGを作成し、C#側でPNG化して出力します。

(直接RDKitライブラリからPNGを作成しないのは、Cairoが必要になり依存関係が増えてしまうため(なぜかCairoの依存先のlibiconvがvcpkgでAndroidビルド出来ない))

例のごとくCドライブ直下にRDKitCSharpCliフォルダを作成してプロジェクトの作成を行います。
C#側のライブラリとしてSkiaSharpの追加も行います。
前節で作成したラッパーライブラリの配置もここに行います。
(後述のdotnet runで特に設定せず読み込んでくれるため)

```
cd c:
mkdir RDKitCSharpCli
cd RDKitCSharpCli
dotnet new console -lang C#
dotnet add package SkiaSharp.Svg
dotnet add package SkiaSharp
cp ../RDKitWrapper/win64/Release/RDKitWrapper.dll .
```

## フォルダ構成

RDKitCSharpCli
├─ Program.cs
├─ RDKitCSharpCli.csproj
└─ RDKitWrapper.dll

### Program.cs

テンプレートで出力されたProgram.csを以下のように変更します。
DLLImportを行うことでDLLライブラリ内の関数を読み出すことができます。

```cs:Program.cs
using System.Runtime.InteropServices;
using System.Text;
using SkiaSharp;

if (args.Length < 2)
{
    var exePath = AppDomain.CurrentDomain.SetupInformation.ApplicationBase;
    var exeName = Path.GetFileName(exePath);
    if (exeName == "")
    {
        exeName = "dotnet run";
    }
    var message = $"usage: {exeName} [SMIILES] [FILE]\n  SMILES: smiles. e.g. c1ccccc1\n  FILE: output png file name. e.g. benzen.png";
    Console.Write(message);
    return;
}

var smiles = args[0];
var filename = args[1];
Int32 bufsize = 1000000;

var stringBuilder = new StringBuilder(bufsize);
try
{
    DrawSVG(smiles, stringBuilder, bufsize);
    var tempPath = Path.Combine(Path.GetTempPath(), "temp.svg");
    File.WriteAllText(tempPath, stringBuilder.ToString());
    var svg = new SKSvg(new SKSize(300, 300));
    svg.Load(tempPath);

    var bitmap = new SKBitmap((int)svg.CanvasSize.Width, (int)svg.CanvasSize.Height);
    var canvas = new SKCanvas(bitmap);
    canvas.DrawPicture(svg.Picture);
    canvas.Flush();
    canvas.Save();

    using (var image = SKImage.FromBitmap(bitmap))
    using (var data = image.Encode(SKEncodedImageFormat.Png, 80))
    using (var stream = File.OpenWrite(filename))
    {
        data.SaveTo(stream);
    }
}
catch (Exception e)
{
    Console.WriteLine(e);
}

[DllImport("RDKitWrapper")]
extern static int DrawSVG(string smiles, [Out] StringBuilder buf, Int32 bufsize);

```

## プロジェクト実行

dotnet runを使うとそのまま実行できます。

```bash
dotnet run c1ccccc1 benzen.png
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/158368/b212f665-506e-41ef-8277-af668050a4da.png)



## ビルドと実行

ビルドは以下のコマンドで実行できます。
ただ、SkiaSharp.dllなどと違ってRDKitWrapper.dllの配置が行われないので、実行ファイルがある位置に手動でコピーする必要があります。

```bash
dotnet build -c Release
cp RDKitWrapper.dll bin/Release/net6.0/
```

```bash
bin/Release/net6.0/RDKitCSharpCli.exe c1ccccc1 benzen.png
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/158368/b212f665-506e-41ef-8277-af668050a4da.png)

# メモ

実はRDKit側で、SWIGでJAVAのクラスライブラリと一緒にC#のクラスライブラリを作成するオプションもあったりします。
ただ、何をやっているか分かりづらいこと、細々とした修正が必要なこと、サイズが大きくなること、必要ない関数まで出力されてしまうなどの理由から、今回は必要な関数だけを持つラッパーライブラリを作成する方向性にしました。

# 次回

C#で実行できるようになったので次回はUnityを使ってゲーム内で実行できるかを記事にしたいと思います。

# 参考

- https://light11.hatenadiary.com/entry/2019/05/25/235638
- https://learn.microsoft.com/en-us/dotnet/standard/native-interop/charset
- https://learn.microsoft.com/en-us/dotnet/standard/native-interop/best-practices