やりたいこと
UnmanagedなC++のdllで、x64用とx86用を作って。dllを使う側に「64ビット環境ならx64用を、32ビット環境ならx86用を使ってください」というのをやめて、1つのdllファイルで、使う環境に合わせて自動でx64/x86を切り替えてくれるようにしたい。
やり方
Costuraを使って、使う側にx64用、x86用のdll両方を組み込んで切り替えることで実現する。
前提
今回は、Costura.Fody4.0.0
を使用して実験する。
サンプルコード
実験イメージ
通常は、dllを使う側のexeと同じ階層に、使うdllが置いてあるイメージ。
(x64とx86があるdllがある場合は、必要な方のdllが置いてあるイメージ。)
Costuraを使った場合は、全部のdllをexeに組み込んで、x64とx86があるものに関しては自動で切り替えてくれるイメージ。
※各図のdllやexeの名前は、今回実験で作ったものを使用。
今回の実験では、一番下のDLL(C++ x64/x86)の足し算をする関数を、一つ上のC#のDLLが呼び、さらにそのC#のメソッドをアプリが呼ぶ、という構成にする。
そのアプリが使う、C#とC++のDLLをexeにまるっと組み込んで、C++のDLLに関しては、x64とx86を両方組み込んで、自動で切り替える、ということをする。
実験でつくるソリューション概要
実験手順
上の実験イメージにある、下記3つを作って実験する。Costura自体は、アプリのところ(アプリ(C#))で使用する。Costura周りだけ知りたいときは、そちら参照
- DLL(C++)(x64用/x86)
- DLL(C#)
- アプリ(C#) ←!!!ここにCosturaを入れる!!!
DLL(C++)(x64用/x86)をつくる
一つ上の層のC#のDLLで呼ばれる関数をつくる。
(C++のDLLの作り方は、以前の記事を参照)
中身は、単に引数を足し合わせるだけ。特殊なプロジェクトの設定等もとくに無し。
#include "pch.h"
#include <windows.h>
#include <stdio.h>
#define VC_DLL_EXPORTS
#include "Dll1.h"
// エクスポート関数の実装
int __cdecl UnmanagedAdd(int a, int b)
{
return a + b;
}
※.hファイルやその他のファイルは、サンプルコードを参照
DLL(C#)をつくる
x64/x86のdllの計算関数を呼んで、結果を返すだけのメソッド。アプリから呼ばれるdll。このdllも、Costuraでexeに組み込まれる。
やっていることは、C++のdllの関数をC#でラップしているだけ。特殊なプロジェクトの設定等もとくに無し。
using System.Runtime.InteropServices;
namespace ClassLibrary1
{
public class Class1
{
public static int Add(int a, int b)
{
return NativeMethod.UnmanagedAdd(a, b);
}
}
public static class NativeMethod
{
[DllImport("Dll1.dll", CallingConvention = CallingConvention.Cdecl)]
public extern static int UnmanagedAdd(int a, int b);
}
}
アプリ(C#)をつくる(Costuraを使う)
C#のdllのメソッドを呼んで、足し算を行う。このプロジェクトで、Costuraを使ってC#のdllとC++のdllをexeに組み込むということをする。
NugetでCosturaを参照に加える
Nugetで「Costura.Fody」と検索し、Costura.Fody v4.0.0
をC#アプリのプロジェクトにインストールする。
C#アプリのプロジェクトにフォルダを追加する
- C#アプリのプロジェクト(ここではConsoleApp10)を右クリックして、[追加] > [新しいフォルダ] を選択する。
- 新しいフォルダができるので、そのフォルダの名前を**「costura32」**に変更する。
- 同じようにして、**「costura64」**フォルダを作成する。
備考
ここでやっているのは、C#アプリのプロジェクトに、x64、x86のdllを配置するためのフォルダを作るという作業。
Costuraが、そこに作成せよと指定している。詳細はこちら参照。
→「ネイティブライブラリを含めるには、ライブラリのビット数に応じて、costura32またはcostura64というフォルダを作成して、埋め込みリソースとして各ビット数のライブラリをプロジェクトに含める必要があります。」とのこと。(訳に自信なし)
作成したフォルダに、x64,x86のdllへのリンクを追加する
- costura32フォルダを右クリックし、[追加] > [既存の項目] を選択する。
- [既存項目の追加]ウインドウで、C++ライブラリの32ビット版の出力フォルダを開く。(ここでは
\<Solutionフォルダ\>\Dll1\bin\Win32\Debug
。事前にC++ライブラリをWin32でビルドしている必要アリ) - そこにある32ビット版のC++のdllを選択
- [追加]ボタンの右の▼をクリックし、[リンクとして追加]を選択する
- 同じようにして、64ビット版も追加する。
追加したリンクを埋め込みリソースにする
- 追加したDLL(へのリンク)を右クリックし、[プロパティ]を選択する
- プロパティの中の[ビルドアクション]を、**[埋め込みリソース]**に設定する
- プロパティの中の[出力ディレクトリにコピー]を、**[コピーしない]**に設定する
追加したリンクの名前を、x86とx64で共通の名前にする
呼び出し側のアプリのコードは共通で、x64とx86を自動で呼び分けられるように、dllの参照の名前に手を加える。
- エクスプローラでC#アプリのフォルダを開く
- C#アプリの.csprojファイル(ここでは
ConsoleApp10.csproj
)をエディタで開く - x64,x86のdllへのリンク部分を、下記のように直す。
<ItemGroup>
<EmbeddedResource Include="..\Dll1\bin\Win32\Debug\Dll1_Win32.dll"> ★実物はx86の出力物
<Link>costura32\Dll1.dll</Link> ★ここが、x64とx86で共通の名前になる
</EmbeddedResource>
<EmbeddedResource Include="..\Dll1\bin\x64\Debug\Dll1_x64.dll"> ★実物はx64の出力物
<Link>costura64\Dll1.dll</Link> ★ここが、x64とx86で共通の名前になる
</EmbeddedResource>
<Content Include="FodyWeavers.xml" />
</ItemGroup>
これで、C++のdllを呼ぶ側からは共通のコードでx64,x86のdllを使えるようになる。そのコードの該当部分は下記。
public static class NativeMethod
{
// ★ここで、「Dll1_Win32.dll」「Dll1_x64.dll」とか分けずにDll1を使える!!
[DllImport("Dll1.dll", CallingConvention = CallingConvention.Cdecl)]
public extern static int UnmanagedAdd(int a, int b);
}
FodyWeavers.xmlを修正する
Costura.Fodyをインストールすると、自動的にソリューションフォルダにFodyWeavers.xml
が作成される。
デフォルトでは下記のようになっている。
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<Costura />
</Weavers>
これを、下記のように修正する。
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<Costura>
<IncludeAssemblies >
ClassLibrary1
</IncludeAssemblies >
<Unmanaged32Assemblies>
Dll1_Win32
</Unmanaged32Assemblies>
<Unmanaged64Assemblies>
Dll1_x64
</Unmanaged64Assemblies>
</Costura>
</Weavers>
各部分の意味は、下記の通り。
tag | 意味 |
---|---|
<IncludeAssemblies> | Costuraを使ってexeに埋め込みたいManagedのdllをここに書く。(「.dll」は書かずに名前だけ。) |
<Unmanaged32Assemblies> | Costuraを使ってexeに埋め込みたいUnManagedの32ビットのdllをここに書く。 |
<Unmanaged64Assemblies> | Costuraを使ってexeに埋め込みたいUnManagedの64ビットのdllをここに書く。 |
他にも、exeに組み込まないようにしたりもできる。
FodyWeavers.xmlについての詳細は、Costuraのページの、Consiguration Optionsの部分を参照。
FodyWeavers.xmlをC#アプリのプロジェクトに加えておく
FodyWeavers.xmlはVisualStudio上でプロジェクトに加えなくても、そこにあるだけでCostura的に動いてくれるが、dllを増やしたときなどにはメンテしないといけないものなので、見えやすいようにプロジェクトに加えておく。(必須ではない)
その他必要事項やメモなど
- 各アプリやdllは、それが使用するdllがないとビルドできなかったり動作しないので、下から順番にビルドするように依存関係を設定しておくこと。
- C++のプロジェクト作成時、デフォルトではX64とx86(Win32)の出力先フォルダは同じところになっていたと思うが、わかりやすいようにx64とx86(Win32)で別のフォルダになるように変えている。Costuraも、その別にしたフォルダを参照させている。
- デバッグでstep実行などをするにはpdbファイルが必要になるので、ビルド後コマンドなどを使ってpdbがexeと同じフォルダにコピーされるようにする。
実験完了
以上で、exeの中にx64とx86のdllを組み込んで、アプリが動作する。
とくに**「FodyWeavers.xmlを修正する」**あたりがややこしくて何をしているか最初の頃わからなくなっていたので、実施する際は今回のような仮のソリューションを作って練習した方がいいと思う。
★別解(19/07/31追記)(むしろこちらの方が、csprojファイルを直接いじったりしないのでお勧め)
いろいろ調べているうちに、個人的によりよいと思えるやり方を見つけたので記載しておく。(参考)
元のやり方で**「とくに「FodyWeavers.xmlを修正する」あたりがややこしくて...」**と思っていたリンクを作る当たりの作業を行わなくて済む。こちらは基本的に同じことをするが、下記の点が異なる。
-
DLL(C++)(x64用/x86)をつくるのところで、x63,86の出力ファイルの名前を別のもの(Dll1_Win32.dll/Dll1)
_x64.dllにしていたが、それをおなじ名前に戻す(どちらもDll1.dllにする)
- DLL(C++)(x64用/x86)をつくるのところで、「ビルド後イベント」にC++のdllをC#アプリのcostura32/64フォルダにコピーするコマンドを入れる
- 作成したフォルダに、x64,x86のdllへのリンクを追加するのところで、リンクを追加ではなく、costura32/64フォルダにコピーされている実体のファイルを埋め込みリソースとして追加する
- FodyWeavers.xmlを修正するのところで、Dll1_Win32、Dll1_x64、と分けていた名前を、どちらもDll1にする
という風にすると、元のやり方の方で、C#アプリの.csprojファイルをエディタで選んで直接編集してたような、変なことをしなくてよいので、やり方としてはこちらの方がきれいな気がする。(実体のdllがソリューション内に複数できてしまうのはデメリットといえばデメリット)
一応、2.3.4を行ったときのバッチのサンプルやFodyWeavers.xmlの一部を上げておく。
2.のビルド後イベントの中身
rem C++の出力ファイルのdllを、C#アプリのプロジェクトにあるcostura32/64フォルダにコピーする
rem costura32/64にあるアンマネージドのC++dllが、exeに組み込まれる
if $(Platform) == Win32 (
xcopy /Y /I $(SolutionDir)Dll1\bin\$(Platform)\$(Configuration)\*.dll $(SolutionDir)ConsoleApp10\costura32\
xcopy /Y /I $(SolutionDir)Dll1\bin\$(Platform)\$(Configuration)\*.pdb $(SolutionDir)ConsoleApp10\costura32\
)
if $(Platform) == x64 (
xcopy /Y /I $(SolutionDir)Dll1\bin\$(Platform)\$(Configuration)\*.dll $(SolutionDir)ConsoleApp10\costura64\
xcopy /Y /I $(SolutionDir)Dll1\bin\$(Platform)\$(Configuration)\*.pdb $(SolutionDir)ConsoleApp10\costura64\
)
3.の設定をしたときのソリューションエクスプローラの表示とcsprojの中身の一部
4.で修正したFodyWeavers.xml
※ただ、この<Unmanaged32Assemblies>
と<Unmanaged64Assemblies>
は、直さなくても、というか、何と書かれていても、もっと言うと、この2つがなくても、うまくexeにx64,86のdllは組み込まれてる気がする..(costura32/costura64フォルダにさえ入っていれば、組み込まれてくれる??)
元のやり方と別解の保存場所
ちょっと色々ごちゃごちゃ書いたので、コードを見た方がはやいかもしれない。下記を見て、別解を試すときは比較してやれば、何が変わったかわかりやすいと思う。
■元のやり方
x64,86のdllをcostura32/64フォルダにリンクとして追加する方法
https://github.com/tera1707/WPF-/commit/71d0938d6c74b7190a423e418fd041fcff6e45fb#diff-cce630225d448987223f968060114623
■別解
x64,86のdllをcostura32/64フォルダに実体コピーして使う方法
https://github.com/tera1707/WPF-/commit/4459dc07eea73d7d76813eb7bf2b3b6de61bbf0d#diff-cce630225d448987223f968060114623
参考
Costura Github
https://github.com/Fody/Costura
【C++/C#】C++で作成したDLLをC#で呼ぶ
https://qiita.com/tera1707/items/81042abaa62dc97e26ae
Putting DLLs Into a Single WPF Exe
https://blog.jonstodle.com/putting-dlls-into-a-single-wpf-exe/