前回
今回
前回はC++で直接コンソールアプリを作成しましたが、今回はラッパー関数のダイナミックライブラリを作成して別言語(C#)でコンソールアプリを作成します。
開発環境の構築
- Visual Studio 2019
- vcpkg
- dotnet
- Visual Studio Installerから「.NET デスクトップ開発」を追加してください
RDKitライブラリを使ってラッパーライブラリを作成
RDKitのスタティックライブラリを利用して、C#で利用するラッパー関数を持つダイナミックライブラリを作成します。
この際、RDKitのスタティックライブラリのビルドが終わっている必要があるので、前回の記事の「rdkitライブラリ(static)のビルド」まで進めてください。
フォルダ構成
RDKitWrapperとしてライブラリを作成します。
フォルダ構成は以下の通りです。
簡単のため、例のごとくCドライブ直下に作成します。
RDKitWrapper
├─ CMakeLists.txt
├─ rdkit_wrapper.cpp
└─ rdkit_wrapper.def
CMakeLists.txt
プロジェクト名以外前回のコンソールアプリとほぼ変わりませんが、add_executableがadd_libraryになっており、ライブラリを作成する設定になっています。
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を書き込む形にしており、マーシャリングも行っています。
エラーも外部から読み込むことが難しいので、標準エラー出力にメッセージを出力する形にしています。
(もし略)
#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下の関数が外部から読み込めます。
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]
cd win64
msbuild ALL_BUILD.vcxproj /p:Configuration=Release
ラッパーライブラリの構成の確認
依存性の確認
作成したライブラリに必要ない依存性が入っていないか確認します。
開発者用のPowerShellのまま以下のコマンドを実行します。
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のまま以下のコマンドを実行します。
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ライブラリ内の関数を読み出すことができます。
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を使うとそのまま実行できます。
dotnet run c1ccccc1 benzen.png
ビルドと実行
ビルドは以下のコマンドで実行できます。
ただ、SkiaSharp.dllなどと違ってRDKitWrapper.dllの配置が行われないので、実行ファイルがある位置に手動でコピーする必要があります。
dotnet build -c Release
cp RDKitWrapper.dll bin/Release/net6.0/
bin/Release/net6.0/RDKitCSharpCli.exe c1ccccc1 benzen.png
メモ
実はRDKit側で、SWIGでJAVAのクラスライブラリと一緒にC#のクラスライブラリを作成するオプションもあったりします。
ただ、何をやっているか分かりづらいこと、細々とした修正が必要なこと、サイズが大きくなること、必要ない関数まで出力されてしまうなどの理由から、今回は必要な関数だけを持つラッパーライブラリを作成する方向性にしました。
次回
C#で実行できるようになったので次回はUnityを使ってゲーム内で実行できるかを記事にしたいと思います。