- 投稿日:2020-11-30T23:42:39+09:00
【C#】WordファイルをPDFに一括変換する.NETアプリを作ってみた
タイトル通りの内容ですが、内部で使用している変換には
Microsot.Office.Interop.Word
を使用しています。Google先生に聞くとこの辺りの情報は豊富ですし、COMオブジェクト故に「
Microsot.Office.Interop.Word
は使うな!」という意見も見つかりますが、PDF保存には必要な選択肢なので避けることはできません(ファイルの読書きだけならマネージドなライブラリを使用してもよさそうですが)。あと、保証が無いオープンソースのライブラリを使用することの是非もあります。個人で使用するならともかく、エンタープライズ向けアプリならその視点が必要では。まぁ、有償であればPDF変換ライブラリはあるかもしれませんが。
本筋ではない補足ですが、OfficeファイルのPDF変換は他にも方法があり、Officeファイルの一括変換はAcrobat有償版で一応はできるのと、2つ目はやや面倒な方法ですが、プリンタドライバーの「Microsoft Print to PDF」を使用することも可能です。後者はWindows10に標準搭載されていますが、保存するファイル1つ1つの名称を都度聞いてくるので、やや手間です。
なので、同階層にあるWordファイルを一括変換するアプリを作ってみようと思いました。それが経緯です。
※ちなみに、これに関連して、ガベージコレクションのコーディング例を検証した記事は以下になります。
必要な環境
- .NET Framework 4.6.1 or later
- MS Wordがインストールされていること(2013, 2019のWordで確認)
- OSはWindows 8.1と10で確認済
画面
アップロード先
一応、ソースをGitHubにアップロードしています。
英語が下手なのはご容赦下さい。
注意点
マイドキュメントなどをリダイレクトしている場合(VDI環境でやるやつ)、それらの特殊フォルダの中で実行するとパスが上手くとれないと思います(¥¥で始まるネットワークパスが入るはず)。この辺りは要調査中。
開発に当たっての所感等
Microsfot.Office.Interop.Word
がCOMオブジェクトで、パフォーマンス及びガベージコレクションに関する問題があることは一応は知っていたので、マネージドなライブラリがないか探したが、見つからず(有償ならあるようだが)。Word, ExcelファイルはそもそもがOffice Open XMLと呼ばれる規格で実態はXMLで構成されているが、NuGetで公開されている他のOSSライブラリに関しても、あくまでファイルの読書きができるのみで、PDF変換はできない(考えれば分かることだが)。
従って、あまり使用したくはなかったが、この選択に落ち着いた。
.NETからCOMオブジェクトを使用する際に注意すべきガーベジコレクション(GC)の動作については、別のQiita記事で公開する予定。
- 投稿日:2020-11-30T23:05:53+09:00
[C#] レジストリに好きな値を書き込む
目次
https://qiita.com/tera1707/items/4fda73d86eded283ec4fやりたいこと
レジストリの中の、指定したキーのサブキーの値を好きな値に書き換えたい。
具体的には、WPFアプリ(デスクトップアプリ)で、アクションセンターに入っても動かせるトーストのアプリを作りたいので、
コンピューター\HKEY_CURRENT_USER\Software\Classes\CLSID\{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}\LocalServer32
のようなキー/サブキーに、自分の動かしたいexeのパスを書き込みたい。
(xxxxx~の部分は、GUIDが入るつもり)ゴールのイメージはこんな感じ。(下のGUIDはVSで適当に今つくったGUID)
やり方
var key = Registry.CurrentUser.CreateSubKey(キーの名前)
と、
key.SetValue(null, 書きたい値)
をやる。サンプル
HKEY_CURRENT_USER
の中の、「Target Key」で指定したサブキーの中に、
「Value」で指定した文字列をセットするだけのプログラム。とりあえず今やりたいことは、上記の「Target Key」の欄の
{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}
の部分を、ほんとのGUIDに変えてあげて書き込みすれば、達成はできそう。■サンプルコード
MainWindow.xaml<Window x:Class="MakeShortCut.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:MakeShortCut" mc:Ignorable="d" Title="MainWindow" SizeToContent="Height" Width="800"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <TextBlock Text="HKEY_CURRENT_USER" Grid.Row="0" Grid.Column="1"/> <TextBlock Grid.Row="1" Text="Target Key"/> <TextBox Name="tbTargetKey" Grid.Row="1" Grid.Column="1"/> <TextBlock Grid.Row="2" Text="Value"/> <TextBox Name="tbValue" Grid.Row="2" Grid.Column="1"/> <Grid Grid.Row="4" Grid.ColumnSpan="2"> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition /> </Grid.ColumnDefinitions> <Button Name="make" Grid.Column="0" Content="書き込み" Click="make_Click" /> <Button Name="read" Grid.Column="1" Content="読み込み" Click="read_Click"/> </Grid> </Grid> </Window>MainWindow.xaml.csusing Microsoft.Win32; using System; using System.Windows; namespace MakeShortCut { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); tbTargetKey.Text = @"SOFTWARE\Classes\CLSID\{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}\LocalServer32"; tbValue.Text = System.Reflection.Assembly.GetExecutingAssembly().Location; } private void make_Click(object sender, RoutedEventArgs e) { using (var key = Registry.CurrentUser.OpenSubKey(tbTargetKey.Text)) { // すでに指摘のキーに指定の値がある場合は、なにもせず終わりたい if (string.Equals(key?.GetValue(null) as string, tbValue.Text, StringComparison.OrdinalIgnoreCase)) { MessageBox.Show("すでに同じ値が登録済みです"); return; } } using (var key = Registry.CurrentUser.CreateSubKey(tbTargetKey.Text)) { // 書き込み実施 key.SetValue(null, tbValue.Text); } } private void read_Click(object sender, RoutedEventArgs e) { using (var key = Registry.CurrentUser.OpenSubKey(tbTargetKey.Text)) { if (key == null) { MessageBox.Show("キーがありません"); } else { // 読み込んで画面に表示 tbValue.Text = (string)key.GetValue(null); } } } } }参考
レジストリへの書き込み、読み込み、削除を行う
レジストリの書き込みのやり方なら、このページ見るだけでOK
https://dobon.net/vb/dotnet/system/registrykey.htmlx64 Windows でのレジストリの扱い
x64とx86、AnuCPUで、書き込む先のキーがなんか変わる(WOW6432の中になるかならないか)は理由はコレ
https://clown.cube-soft.jp/entry/20100331/1270016634
このページによると、下記のようになるとのこと。(実際に試してもそうなった)
- 投稿日:2020-11-30T22:31:59+09:00
Unity WebGL C# <=> JS (jslib)
Unity (#1) Advent Calendar 第1日目を飾るにはものすごくふさわしくない超地味な内容となっています。申し訳ございません。
以前に
Unity(WebGL)でC#の関数からブラウザー側のJavaScript関数を呼び出すまたはその逆(JS⇒C#)に関する知見(プラグイン形式[.jslib])
という長ったらしいタイトルの記事を書きましたが、今回はこれの更新版+αという内容となっています。
前回の記事はもう古くなってしまったので、改めて調査をしました。
(古い記事は一応古いバージョンとして残しておきます)調査したUnityのバージョン: 2019.4.1f1 (と2020.2.0b2.3094)
(文中ではそれぞれ 2019, 2020 と省略して表記します)どうしてもES6+でコードが書けない
なんかemscriptenの最新バージョンだとES6+でコードが書けるようになったとかならないとか。ただしUnityで使用されているemscriptenではいまだ(Unity 2020)にES6+でコード書くと怒られます。
同じ関数名の関数はどちらかが上書きされる
複数の.jslibを用意してコンパイルすると、同じスコープ(ブロック)に展開されます。
なので、別々の.jslibファイルであっても同じ関数名の関数を定義した場合、どちらかが上書きされてしまいますので注意が必要です。ですので、名前が被らないような少し長めな関数名にすることがいいでしょう。
(どのような順番で上書きされるのかまでは未調査)
特に、アセットストアにあるWebGL用のアセットをインポートすると高確率で.jslibファイルがありますので、もしかするとこういったアセットの.jslibの関数を上書きしてしまう可能性があることに注意してください。逆に、これがとても有効に働くときもあります。それがUnity自身が用意している.jslib(実際は.js)の上書きです。
例えば、WebCamTextureのWebGLビルド用ソースはWebCam.jsというファイルになっています。ただ、このWebCam.jsは複数カメラが接続された状態でのカメラの選択などが行えないなどの非情なまでのバグがあります。このWebCam.jsで定義されている関数と同じ関数名で正しく動作する関数を定義した.jslibファイルを用意してコンパイルすればきちんとカメラデバイスを選択できるようになります。
きちんとデバイスを選択できるようにしたサンプルdynCallパターン
dynCallのパターンが2019では 165パターンに結構増えており、さらに2020においては566パターンとめちゃくちゃ増えてます。
ちなみに、C:\Program Files\Unity\Hub\Editor[version]\Editor\Data\PlaybackEngines\WebGLSupport\BuildTools\Emscripten にあるemscripten-version.txtの内容を見ると2019, 2020ともに"1.38.11"となっており一緒でした。
同じバージョンなのになぜパターン数が違うのかが疑問です。dynCallデータ型に'j'が追加される(2019~)
データ型に'j'が追加されています。
ドキュメントから引用させていただくと
- 'v': void type
- 'i': 32-bit integer type
- 'j': 64-bit integer type (currently does not exist in JavaScript)
- 'f': 32-bit float type
- 'd': 64-bit float type
となっており'j'はBigIntとして扱うのでしょう。
(なぜ'j'なのかをDiscordで聞いてみたら'i'の次だからそうです)
とすると、HEAP64やHEAPU64があるのかと予想しましたが2020でもありませんでした。
このIssueの最後の開発者コメントで、WASM_BIGINTフラグを有効にすることでHEAP64が追加されるということです。
調べてみるとWASM_BIGINTフラグは1.39.13で追加されたもので、試せる環境が手元にないため未検証です。Runtimeオブジェクトの廃止(2019~)
古いバージョンでは、dynCall()などのメソッドはRuntimeオブジェクトにありましたが、このRuntimeが廃止されているようで見当たりませんでした。dynCall()も見当たりません。ですので、dynCall()の代わりに直接dynCall_viといったパターン分用意されたメソッドを使用します。
古いバージョン(2018以前?)でのdynCall_viiの実行// ptrCSFuncは、C#側関数のポインター Runtime.dynCall('vii', ptrCSFunc, [arg1, arg2]);新しいバージョン(2019以降)でのdynCall_viiの実行Module.dynCall_vii(ptrCSFunc, strPtr1, strPtr2);数値配列を渡す(引数)、数値配列を戻す(戻り値)
配列を引数に渡すと.jslib側ではポインターとして受け取ります。ですのでポインターから配列に戻す処理が必要です。
戻り値として配列を戻す場合は、_malloc()したポインターで戻すということをしなければなりません。
固定長配列でしたら、それほど苦労せずに受け渡すことができますが、問題は可変長配列の場合です。
特に戻り値として戻す場合は、1つのデータにしなければなりません。
配列の最初の要素に要素数を追加するという方法も考えたのですが、バイト配列だと最大でも要素数が256までになってしまいますのでこの方法はあまり有効ではありません。頑張って導き出した答えが、最初の4バイトを要素数にし以降を配列のデータとすることでとりあえずできました。
unsafeを使えばある程度すっきりしたコードになりパフォーマンスも上がりますが、ここではあえて(皆さん嫌いな)Marshalを使った方法をとってみました。可変長配列を受け取り、可変長配列を戻すサンプルコード
// Unity [DllImport("__Internal")] private static extern IntPtr byteArrayFunc(byte[] arg, int length); private static byte[] ptrToByteArray(IntPtr ptr) { int len = Marshal.ReadInt32(ptr); byte[] arr = new byte[len]; Marshal.Copy(IntPtr.Add(ptr, 4), arr, 0, len); return arr; } private static void test() { // byte[]を渡し、byte[]の戻り値を受け取る byte[] byteArrayArg = new byte[] { 1, 2, 3 }; IntPtr ptrByteArray = byteArrayFunc(byteArrayArg, byteArrayArg.Length); byte[] byteArrayRet = ptrToByteArray(ptrByteArray); Debug.Log($"byteArrayFunc ret: [{string.Join(", ", byteArrayRet.Select(x => x.ToString()).ToArray())}]"); }// .jslib $utils: { arrayToReturnPtr: function (arr, type) { var buf = (new type(arr)).buffer; var ui8a = new Uint8Array(buf); var ptr = _malloc(ui8a.byteLength + 4); HEAP32.set([arr.length], ptr >> 2); HEAPU8.set(ui8a, ptr + 4); return ptr; }, }, byteArrayFunc: function (arg, len) { debugger; var byteArray = HEAPU8.subarray(arg, arg + len); console.log('byteArrayFunc arg: ' + utils.arrayToString(byteArray)); var ret = [3, 2, 1]; var ptr = utils.arrayToReturnPtr(ret, Uint8Array); return ptr; }_free()するタイミング
前述のサンプルコードを見ていただくと一つ問題に気付いた方もいると思います。
_malloc()したのですから_free()しなければいけません。
戻り値として_malloc()したポインターを戻す場合、いつ_free()するかという問題にあたります。
return ステートメント以降で行わないといけないですが、当然returnステートメント以降は実行されません。
簡単な方法としてはsetTimeout()を使って_free()を実行することで一応、回避可能です。// .jslib //前述のサンプルコードのarrayToReturnPtr関数部分 arrayToReturnPtr: function (arr, type) { var buf = (new type(arr)).buffer; var ui8a = new Uint8Array(buf); var ptr = _malloc(ui8a.byteLength + 4); HEAP32.set([arr.length], ptr >> 2); HEAPU8.set(ui8a, ptr + 4); // setTimeout()で_free()を行う setTimeout(function() { _free(ptr) }, 0); return ptr; }, //...もっと確実な方法としては、面倒ではありますが.jslib側に_free()を行う関数を用意しておき、C#側から戻り値を受け取り用が済んだらその関数を実行することです。
// Unity // _free()を行う関数追加 [DllImport("__Internal")] private static extern void execFree(uint arg); [DllImport("__Internal")] private static extern IntPtr byteArrayFunc(byte[] arg, int length); private static byte[] ptrToByteArray(IntPtr ptr) { int len = Marshal.ReadInt32(ptr); byte[] arr = new byte[len]; Marshal.Copy(IntPtr.Add(ptr, 4), arr, 0, len); // 用が済んだら_free()を行う execFree((uint)ptr); return arr; } private static void test() { // バイト配列を渡し、バイト配列の戻り値を受け取る byte[] byteArrayArg = new byte[] { 1, 2, 3 }; IntPtr ptrByteArray = byteArrayFunc(byteArrayArg, byteArrayArg.Length); byte[] byteArrayRet = ptrToByteArray(ptrByteArray); Debug.Log($"byteArrayFunc ret: [{string.Join(", ", byteArrayRet.Select(x => x.ToString()).ToArray())}]"); }// .jslib // _free()を行う関数を追加 execFree(ptr) { _free(ptr); } byteArrayFunc: function (arg, len) { debugger; var byteArray = HEAPU8.subarray(arg, arg + len); console.log('byteArrayFunc arg: ' + utils.arrayToString(byteArray)); var ret = [3, 2, 1]; var ptr = utils.arrayToReturnPtr(ret, Uint8Array); return ptr; }可変長の文字列配列を渡す、文字列配列を戻す
じゃあ、可変長数値配列の受け渡しができたなら文字列配列も受け渡しできたい。文字コードはUTF8で。
数値配列の受け渡しを応用すれば一応
できました。// Unity [DllImport("__Internal")] private static extern void execFree(uint arg); [DllImport("__Internal")] private static extern IntPtr stringArrayFunc(string[] arg, int length); private static byte[] ptrToByteArray(IntPtr ptr) { Debug.Log($"ptr: {(uint)ptr}"); int len = Marshal.ReadInt32(ptr); Debug.Log($"byteArry len:{len}"); byte[] arr = new byte[len]; Marshal.Copy(IntPtr.Add(ptr, 4), arr, 0, len); execFree((uint)ptr); return arr; } private static string[] ptrToStringArray(IntPtr ptr) { int len = Marshal.ReadInt32(ptr); Debug.Log($"stringArry len:{len}"); IntPtr[] ptrArr = new IntPtr[len]; Debug.Log(ptrArr); Marshal.Copy(IntPtr.Add(ptr, 4), ptrArr, 0, len); List<string> ret = new List<string>(); for (var i = 0; i < len; i++) { var byteArray = ptrToByteArray(ptrArr[i]); var str = Encoding.UTF8.GetString(byteArray); ret.Add(str); } execFree((uint)ptr); return ret.ToArray(); } public static void test() { string[] stringArrayArg = new string[] { "foo", "bar", "baz" }; IntPtr ptrStringArray = stringArrayFunc(stringArrayArg, stringArrayArg.Length); string[] stringArrayRet = ptrToStringArray(ptrStringArray); Debug.Log($"stringArrayFunc ret: [{string.Join(", ", stringArrayRet)}]"); }// .jslib stringArrayFunc: function (arg, len) { var strArray = []; for (var i = 0; i < len; i++) { var ptr = HEAP32[(arg >> 2) + i]; var str = Pointer_stringify(ptr); strArray.push(str); } console.log('strArrayFunc arg: ' + strArray); var ret = ['hoge', 'fuga', 'piyo', 'hogera', 'ほげほげ', '叱る']; var retPtr = utils.stringArrayToReturnPtr(ret); return retPtr; }見ていただくとわかる通り、可変長数値配列の受け渡しもそうですが、可変長文字列配列の受け渡しはさらにめんどいことに。はっきり言ってJSONで受け渡したほうが楽です。
文字列を_malloc()した場合は、Unity側で自動で_free()してくれるのですが、C#側でUTF8に変換したいためにbyte[]に変換しているため自動で_free()されません。
(Marshal.PtrToStringAnsi()で一応、ポインターから文字列に変換することは可能ですがUTF16に変換されてしまいます。.NET5ではMarshal.PtrToStringUTF8()というまんまな関数が用意されましたが、いかんせんUnityでの.NET5のサポートはまだまだ先になるようです)固定長数値配列の参照渡し
UnityのWebXR Exporterというアセットのソースを覗いてたら、お!っと思うコードが記述されていました。
// Unity [DllImport("__Internal")] private static extern void refIntArrayFunc(float[] a, int l); int[] refIntArray = new int[3]; refIntArrayFunc(refIntArray, a.Length);// .jslib refIntArrayFunc: function (arg, len) { Module.refIntArray = new Int32Array(buffer, arg, len); }このように書くことで、C#側のrefIntArrayと.jslib側のModule.refIntArrayは参照渡しの関係となり、.jslib側でModule.refIntArrayの値を変更すると、(returnステートメントなしに)C#のrefIntArrayに値が反映されます。
テクスチャー
テクスチャーは、C#側で生成し、Texture.GetNativeTexturePtr()でポインターを取得し、ポインターを.jslibの関数に渡す。.jslib側でGL.textures[ptr]でテクスチャーを参照することが可能
"らしいです"
。
"らしいです"
というのは、C#でvar texture = new Texture2D(0, 0, TextureFormat.ARGB32, false); var ptr = texture.GetNativeTexturePtr();としても、ptrは0になり有効な値になってくれません。
"もし、有効なポインターを取得する方法をご存じの方がいらっしゃればぜひご教授をお願いします"
仮に有効なポインターの値が取得できた場合は
// .jslib textureFunc(ptr) { GLctx.bindTexture(GLctx.TEXTURE_2D, GL.textures[ptr]); GLctx.pixelStorei(GLctx.UNPACK_FLIP_Y_WEBGL, true); GLctx.texImage2D(GLctx.TEXTURE_2D, 0, GLctx.RGBA, GLctx.RGBA,GLctx.UNSIGNED_BYTE, video); GLctx.pixelStorei(GLctx.UNPACK_FLIP_Y_WEBGL, false); }といったコードを書くことにより、そのテクスチャーにimgエレメントの画像や、videoエレメントの映像、WebRTCなどのMediaStreamの映像などもほぼ直接的に表示できるようになる
"はずです"
。最後に
Unity (#1) Advent Calendar 第1日目の内容は以上となります。
ちょっとネタに走った感はありますが、.jslibを書けるようになればUnityだけではできないこと、特にJS(Web)のいろんなAPIなどをUnityに取り入れることが可能となりますのでぜひかけるようになりましょう!あ、あとまとめたテストコードも載せておきます
// Unity using AOT; using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Runtime.InteropServices; using System.Text; using UnityEngine; public class jslibtest : MonoBehaviour { [DllImport("__Internal")] private static extern void execFree(uint arg); [DllImport("__Internal")] private static extern byte byteFunc(byte arg); [DllImport("__Internal")] private static extern short shortFunc(short arg); [DllImport("__Internal")] private static extern int intFunc(int arg); [DllImport("__Internal")] private static extern float floatFunc(float arg); [DllImport("__Internal")] private static extern double doubleFunc(double arg); [DllImport("__Internal")] private static extern IntPtr byteArrayFunc(byte[] arg, int length); [DllImport("__Internal")] private static extern IntPtr shortArrayFunc(short[] arg, int length); [DllImport("__Internal")] private static extern IntPtr intArrayFunc(int[] arg, int length); [DllImport("__Internal")] private static extern IntPtr floatArrayFunc(float[] arg, int length); [DllImport("__Internal")] private static extern IntPtr doubleArrayFunc(double[] arg, int length); [DllImport("__Internal")] private static extern IntPtr stringArrayFunc(string[] arg, int length); [DllImport("__Internal")] private static extern void refIntArrayFunc(int[] arr, int len); private int[] refIntArray = new int[3]; private void Start() { test(); refIntArrayFunc(refIntArray, refIntArray.Length); StartCoroutine(chekRefArray()); } IEnumerator chekRefArray () { while(true) { yield return new WaitForSeconds(0.3f); Debug.Log($"refIntArray: [{string.Join(", ", refIntArray.Select(x => $"{x}"))}]"); } } private void Update() { } private static byte[] ptrToByteArray(IntPtr ptr) { Debug.Log($"ptr: {(uint)ptr}"); int len = Marshal.ReadInt32(ptr); Debug.Log($"byteArry len:{len}"); byte[] arr = new byte[len]; Marshal.Copy(IntPtr.Add(ptr, 4), arr, 0, len); execFree((uint)ptr); return arr; } private static short[] ptrToShortArray(IntPtr ptr) { int len = Marshal.ReadInt32(ptr); Debug.Log($"shortArry len:{len}"); short[] arr = new short[len]; Marshal.Copy(IntPtr.Add(ptr, 4), arr, 0, len); return arr; } private static int[] ptrToIntArray(IntPtr ptr) { int len = Marshal.ReadInt32(ptr); Debug.Log($"intArry len:{len}"); int[] arr = new int[len]; Marshal.Copy(IntPtr.Add(ptr, 4), arr, 0, len); return arr; } private static float[] ptrToFloatArray(IntPtr ptr) { int len = Marshal.ReadInt32(ptr); Debug.Log($"floatArry len:{len}"); float[] arr = new float[len]; Marshal.Copy(IntPtr.Add(ptr, 4), arr, 0, len); return arr; } private static double[] ptrToDoubleArray(IntPtr ptr) { int len = Marshal.ReadInt32(ptr); Debug.Log($"doubleArry len:{len}"); double[] arr = new double[len]; Marshal.Copy(IntPtr.Add(ptr, 4), arr, 0, len); return arr; } private static string[] ptrToStringArray(IntPtr ptr) { int len = Marshal.ReadInt32(ptr); Debug.Log($"stringArry len:{len}"); IntPtr[] ptrArr = new IntPtr[len]; Debug.Log(ptrArr); Marshal.Copy(IntPtr.Add(ptr, 4), ptrArr, 0, len); List<string> ret = new List<string>(); for (var i = 0; i < len; i++) { var byteArray = ptrToByteArray(ptrArr[i]); var str = Encoding.UTF8.GetString(byteArray); ret.Add(str); } execFree((uint)ptr); return ret.ToArray(); } public static void test() { byte byteArg = 210; byte byteRet = byteFunc(byteArg); Debug.Log($"byteFunc ret: {byteRet}"); short shortArg = 210; short shortRet = shortFunc(shortArg); Debug.Log($"shortFunc ret: {shortRet}"); int intArg = 210; int intRet = intFunc(intArg); Debug.Log($"intFunc ret: {intRet}"); float floatArg = 210.123f; float floatRet = floatFunc(floatArg); Debug.Log($"floatFunc ret: {floatRet}"); double doubleArg = 210.321d; double doubleRet = doubleFunc(doubleArg); Debug.Log($"doubleFunc ret: {doubleRet}"); byte[] byteArrayArg = new byte[] { 1, 2, 3 }; IntPtr ptrByteArray = byteArrayFunc(byteArrayArg, byteArrayArg.Length); byte[] byteArrayRet = ptrToByteArray(ptrByteArray); Debug.Log($"byteArrayFunc ret: [{string.Join(", ", byteArrayRet.Select(x => $"{x}"))}]"); short[] shortArrayArg = new short[] { 4, 5, 6 }; IntPtr ptrShortArray = shortArrayFunc(shortArrayArg, shortArrayArg.Length); short[] shortArrayRet = ptrToShortArray(ptrShortArray); Debug.Log($"shortArrayFunc ret: [{string.Join(", ", shortArrayRet.Select(x => $"{x}"))}]"); int[] intArrayArg = new int[] { 7, 8, 9 }; IntPtr ptrIntArray = intArrayFunc(intArrayArg, intArrayArg.Length); int[] intArrayRet = ptrToIntArray(ptrIntArray); Debug.Log($"intArrayFunc ret: [{string.Join(", ", intArrayRet.Select(x => $"{x}"))}]"); float[] floatArrayArg = new float[] { 1.1f, 2.2f, 3.3f }; IntPtr ptrFloatArray = floatArrayFunc(floatArrayArg, floatArrayArg.Length); float[] floatArrayRet = ptrToFloatArray(ptrFloatArray); Debug.Log($"floatArrayFunc ret: [{string.Join(", ", floatArrayRet.Select(x => $"{x}"))}]"); double[] doubleArrayArg = new double[] { 5.5d, 6.6d, 7.7d }; IntPtr ptrDoubleArray = doubleArrayFunc(doubleArrayArg, doubleArrayArg.Length); double[] doubleArrayRet = ptrToDoubleArray(ptrDoubleArray); Debug.Log($"doubleArrayFunc ret: [{string.Join(", ", doubleArrayRet.Select(x => $"{x}"))}]"); string[] stringArrayArg = new string[] { "foo", "bar", "baz" }; IntPtr ptrStringArray = stringArrayFunc(stringArrayArg, stringArrayArg.Length); string[] stringArrayRet = ptrToStringArray(ptrStringArray); Debug.Log($"stringArrayFunc ret: [{string.Join(", ", stringArrayRet)}]"); } }var lib = { $utils: { arrayToString: function (arr) { var ret = '['; for (var i = 0; i < arr.length; i++) { var spl = i === arr.length - 1 ? '' : ', '; ret += arr[i].toString() + spl; } return ret + ']'; }, arrayToReturnPtr: function (arr, type) { var buf = (new type(arr)).buffer; var ui8a = new Uint8Array(buf); var ptr = _malloc(ui8a.byteLength + 4); HEAP32.set([arr.length], ptr >> 2); HEAPU8.set(ui8a, ptr + 4); // setTimeout(function() { _free(ptr) }, 0); return ptr; }, stringArrayToReturnPtr: function (strArr) { var ptrArray = []; var enc = new TextEncoder(); for (var i = 0; i < strArr.length; i++) { var byteArray = enc.encode(strArr[i]); var ptr = utils.arrayToReturnPtr(byteArray, Uint8Array); ptrArray.push(ptr); } var ptr = utils.arrayToReturnPtr(ptrArray, Uint32Array); return ptr; } }, execFree: function (ptr) { console.log('free ptr: ' + ptr); _free(ptr); }, byteFunc: function (arg) { console.log('byteFunc arg: ' + arg); var ret = 128; return ret; }, shortFunc: function (arg) { console.log('shortFunc arg: ' + arg); var ret = 128; return ret; }, intFunc: function (arg) { console.log('intFunc arg: ' + arg); var ret = 128; return ret; }, longFunc: function (arg) { console.log('longFunc arg: ' + arg); var ret = 128; return ret; }, floatFunc: function (arg) { console.log('floatFunc arg: ' + arg); var ret = 128.123; return ret; }, doubleFunc: function (arg) { console.log('doubleFunc arg: ' + arg); var ret = 128.123; return ret; }, byteArrayFunc: function (arg, len) { var byteArray = HEAPU8.subarray(arg, arg + len); console.log('byteArrayFunc arg: ' + utils.arrayToString(byteArray)); var ret = [3, 2, 1]; var ptr = utils.arrayToReturnPtr(ret, Uint8Array); console.log('jslib ptr: ' + ptr); return ptr; }, shortArrayFunc: function (arg, len) { var shortArray = HEAP16.subarray(arg, len); console.log('shortArrayFunc arg: ' + shortArray); var ret = [6, 5, 4]; var ptr = utils.arrayToReturnPtr(ret, Int16Array); return ptr; }, intArrayFunc: function (arg, len) { var intArray = HEAP32.subarray(arg, len); console.log('intArrayFunc arg: ' + intArray); var ret = [9, 8, 7]; var ptr = utils.arrayToReturnPtr(ret, Int32Array); return ptr; }, floatArrayFunc: function (arg, len) { var floatArray = HEAPF32.subarray(arg, len); console.log('floatFunc arg: ' + floatArray); var ret = [3.3, 2.2, 1.1]; var ptr = utils.arrayToReturnPtr(ret, Float32Array); return ptr; }, doubleArrayFunc: function (arg, len) { var doubleArray = HEAPF64.subarray(arg, len); console.log('doubleFunc arg: ' + doubleArray); var ret = [6.6, 5.5, 4.4, 3.3, 2.2]; var ptr = utils.arrayToReturnPtr(ret, Float64Array); return ptr; }, stringArrayFunc: function (arg, len) { var strArray = []; for (var i = 0; i < len; i++) { var ptr = HEAP32[(arg >> 2) + i]; var str = Pointer_stringify(ptr); strArray.push(str); } console.log('strArrayFunc arg: ' + strArray); var ret = ['hoge', 'fuga', 'piyo', 'hogera', 'ほげほげ', '叱る']; var retPtr = utils.stringArrayToReturnPtr(ret); return retPtr; }, refIntArrayFunc: function (arg, len) { console.log('ref len:' + len); Module.refIntArray = new Int32Array(buffer, arg, len); Module.sampleValue = 0; setInterval(function () { console.log('refIntArray update: ' + Module.refIntArray.length + ' ' + Module.sampleValue ); for (var i = 0; i < Module.refIntArray.length; i++) { Module.refIntArray[i] = Module.sampleValue + i; } Module.sampleValue += Module.refIntArray.length; }, 1000); } }; autoAddDeps(lib, '$utils'); mergeInto(LibraryManager.library, lib);
- 投稿日:2020-11-30T16:42:54+09:00
ScriptableWizardからコンポーネントとプレハブを同時に作成する例
PONOS Advent Calendar 2020の2日目の記事です。
昨日は@kerimekaさんの【合格】Googleの認定資格、PCA(Professional Cloud Architect)を取得した話でした。
はじめに
最近、エディタ拡張機能の
ScriptableWizard
を使用しアセットの作成を効率化する機会があったのですが、作業の中で少し工夫が必要な部分があったので記録として残しておこうと思います。なお、本記事の実行環境は
Unity 2019.4.8f1
となります。作成するもの
この記事で取り上げる成果物は「画面UIのプレハブと制御用のコンポーネントを同時に作成するウィザード」です。
例えば、ゲームを開発している中でタイトル画面表示用のプレハブと、その制御用のコンポーネントのスクリプトを同時に作成したいシチュエーションがあると思います。
単純な操作なので手作業で作成していくのもアリなのですが、「タイトル画面」以外にも「ホーム画面」や「メニュー画面」のように画面数が増えていくとその単純作業のコストも馬鹿にならず、また作成されるプレハブの構造やコンポーネントの内容を統一したい、など作成ルールが複雑化すると量産作業に余計な時間と神経を使うことになってしまいます。そこで今回は、画面名(たとえば
Title
)を指定して作成処理を実行するだけで、その画面名にあったプレハブ(Title.prefab
)と制御用のコンポーネント(TitleViewController.cs
)が同時に作成される機能を目指しました。プレハブへの制御用コンポーネントの追加を手動で実施する必要が無いように、制御用のコンポーネントがあらかじめプレハブに追加された状態にしておきます。
この機能を使用することで画面の量産作業は画面名を入力してボタンをクリックするだけとなり、負担を大幅に軽減できます。画像でも補足しておきます。
以下のような作成ウィザードが立ち上がり、作成したい画面名を「View Name」フィールドに入力して「Create」ボタンをクリックすることで、
以下のようにプレハブとそれを制御するためのコンポーネントが作成されます。
ScriptableWizardについて
まずはじめに
ScriptableWizard
について少し触れておきます。UnityEditor.ScriptableWizard - Unity スクリプトリファレンス
ScriptableWizard
は「何かを作成するエディタウインドウ」を作成することに特化したエディタウインドウです。
以下のような特徴を持ちます。
- シリアライズ可能なフィールドがエディタウインドウ上の入力フィールドとして表示される
- 作成処理を実行するための「Create」ボタンがエディタウインドウ上に予め設置されている
ゲームオブジェクトやプレハブ、その他のアセットなどをパラメータ指定して作成するのに便利な機能となっています。
作成の手順
手順1. ScriptableWizardを作成する
まずは、
ScriptableWizard
クラスを継承したクラスを作成します。
ScriptableWizard
クラスはEditorWindow
クラスを継承しているため、通常のエディタウインドウを作成する時と作業はほとんど同じです。エディタウインドウの作成を経験されている場合、ここは特に難しい点はないと思います。今回は画面を作成するウィザードということで「ViewCraeteWizard」というクラス名で以下のようなコードを作成しました。
using UnityEngine; using UnityEditor; public class ViewCreateWizard : ScriptableWizard { [SerializeField] string viewName = string.Empty; [MenuItem("View/Create View")] static void CreateWizard() { // 作成ウィザードを表示する。 DisplayWizard<ViewCreateWizard>("View Create Wizard"); } }とてもシンプルなコードですが、簡単に作成ウィザードのエディタウインドウを作成することができました。
GUIについてコーディングせずとも入力フィールドや作成ボタンが自動的に実装されており、非常にお手軽です。手順2. コンポーネントを作成する
StringBuilder
を利用してコードの文字列を作成し、UnityプロジェクトのAssetsフォルダ以下にC#スクリプトを作成します。
ScriptableWizard
クラスを継承したクラスでは、OnWizardCreate()
メソッド内にウィザードの「Create」ボタンがクリックされた時の処理を記述することができます。void OnWizardCreate() { string className = $"{viewName}ViewController"; string path = $"{Application.dataPath}/{className}.cs"; var builder = new StringBuilder(); builder.AppendLine("using UnityEngine;"); builder.AppendLine(); builder.AppendLine($"public class {className} : MonoBehaviour"); builder.AppendLine("{"); builder.AppendLine("}"); // 文字列を指定のパスに書き出す。 File.WriteAllText(path, builder.ToString()); // Unityのプロジェクトに反映する。 AssetDatabase.Refresh(); }
File.WriteAllTtext()
メソッドでスクリプトを書き出した後にAssetDatabase.Refresh()
を呼び出しておかないと、UnityのProjectウインドウ上に即時反映されないため注意してください。これで、以下のようにウィザードを入力して「Create」ボタンをクリックすると、
以下のようなファイルが作成されるようになりました。
また、ゲームオブジェクトの「Add Component」から選択することも可能です。
手順3. プレハブを作成する
次に、プレハブを作成します。
プレハブの作成には以下のようなコードを用意します。手順2のOnWizardCreate()
メソッドに追記しています。var gameObject = new GameObject(viewName); var prefabPath = $"{viewName}.prefab"; // 指定したゲームオブジェクトをプレハブ化する。 PrefabUtility.SaveAsPrefabAsset(gameObject, prefabPath); // プレハブ化したゲームオブジェクトをHierarchyから破棄する。 GameObject.DestroyImmediate(gameObject);
PrefabUtility
クラスのSaveAsPrefabAsset()
を使用することで、引数のゲームオブジェクトをプレハブ化することができます。
なお、SaveAsPrefabAsset()
を実行するためにはHierarchy上にゲームオブジェクトを作成する必要があるのでnew GameObject()
していますが、プレハブ化した後は不要となるのでGameObject.DestroyImmediate()
で削除しておきましょう。この状態で作成処理を実行すると、コンポーネントとともにプレハブが作成されるようになりました。
手順4. プレハブへコンポーネントを追加する
さて、手順3でプレハブは作成できましたが、手順2で作成したコンポーネントをまだ追加できていません。
ここで少し工夫が必要となります。この手順を進めるためにはさらに2点の対応が必要となりました。
- 動的に作成されたコンポーネントの型を特定し、コンポーネントを追加する
- コンパイルが終了してからコンポーネントを取得する
動的に作成されたコンポーネントの型を特定し、コンポーネントを追加する
ViewCreateWizard
で動的に作成された画面制御用のコンポーネントをプレハブへ追加したいのですが、動的に作成されるコンポーネントの型を直接参照することができないため、ジェネリックで追加するコンポーネントの型を指定するGameObject.AddComponent<T>()
は利用できません。
そこで今回は、Type
オブジェクトを引数にしてコンポーネントの追加が行えるGameObject.AddComponent(Type componentType)
を使用していこうと思います。Type
オブジェクトはクラス名の文字列から取得する想定です。このアプローチであれば、コンポーネントの型を直接参照できなくても、指定した型のコンポーネントの追加を実現できます。
(なお、同様にタイプ名文字列からコンポーネントの追加を行えるGameObject.AddComponent(string className)
も存在しますが、こちらは非推奨になっているため採用しません)以下のコードでは
Assembly
クラスを利用して、クラス名をキーにType
オブジェクトを取得しています。
OnWizardCreate()
メソッドに追記しています。var gameObject = new GameObject(viewName); // Assembly-CSharpアセンブリからクラスを取得する。 var assembly = Assembly.Load("Assembly-CSharp"); var componentType = assembly.GetType(className); gameObject.AddComponent(componentType);この処理でならプレハブへコンポーネントの追加ができそうです。
しかし、実はこの時点では動的に作成されたコンポーネントのコンパイルが完了していないため、アセンブリからType
オブジェクトを取得することができません。コンポーネントの追加に「AddComponent asking for invalid type」の警告が発生して処理に失敗します。コンパイルが終了してからコンポーネントを取得する
続いてはコンパイルが通ったあとにこれらの
Type
オブジェクトの取得処理がが実行されるように修正していきます。主な方針は、以下です。
- コンパイルの完了後に
ViewCreateWizard
クラスが再読み込みされるので、その時に作成処理を再開する。- クラスの再読み込み時にクラスに設定されたパラメータが破棄されてしまうので、EditorPrefsへ作成に関する情報を保存しておく。
まずは、
ViewCreateWizard
クラスへInitializeOnLoad
属性を付与します。
これにより、再コンパイル完了後にstaticコンストラクタによる初期化処理が実行されるようになります。
staticコンストラクタ内ではプレハブ作成処理を再開するためのメソッド(ここではOnCompilationFinished()
メソッドとする)を呼び出すようにしておきます。[InitializeOnLoad] public class ViewCreateWizard : ScriptableWizard { static ViewCreateWizard() { OnCompilationFinished(); }クラスの再読み込みが実施された時にクラス内のパラメータが破棄されてしまうので、作成処理の再開時に情報を引き継ぐためにあらかじめEditorPrefsへ作成に関する情報を逃がしておきます。
今回、画面の作成に必要な情報は画面名だけなので、そちらをEditorPrefs.SetString()
で保存しておきます。void OnWizardCreate() { // 作成情報をEditorPrefsへ保存しておく。 EditorPrefs.SetString("CreatingViewName", viewName); string className = $"{viewName}ViewController"; string path = $"{Application.dataPath}/{className}.cs"; var builder = new StringBuilder(); builder.AppendLine("using UnityEngine;"); builder.AppendLine(); builder.AppendLine($"public class {className} : MonoBehaviour"); builder.AppendLine("{"); builder.AppendLine("}"); File.WriteAllText(path, builder.ToString()); AssetDatabase.Refresh(); }プレハブの作成処理を再開するメソッドは以下のように記述します。
上で提示したアセンブリからのType
オブジェクトの取得処理を利用し、コンポーネントをプレハブへ追加しています。static void OnCompilationFinished() { if (!EditorPrefs.HasKey("CreatingViewName")) { return; } // 作成情報をEditorPrefsから読み込む。 var creatingViewName = EditorPrefs.GetString("CreatingViewName"); string className = $"{creatingViewName}ViewController"; var gameObject = new GameObject(creatingViewName); var assembly = Assembly.Load("Assembly-CSharp"); var classType = assembly.GetType(className); gameObject.AddComponent(classType); var prefabPath = $"{creatingViewName}.prefab"; PrefabUtility.SaveAsPrefabAsset(gameObject, prefabPath); GameObject.DestroyImmediate(gameObject); // 作成が済んだので、EditorPrefsから削除する。 EditorPrefs.DeleteKey("CreatingViewName"); }ここで改めてウィザードから作成を実行すると、
- コンポーネントのクラスの作成
- コンパイルを待機
- コンパイル終了後にプレハブの追加
が順に実行されます。
以下の画像のように、目的としていた動的に作成されたコンポーネントが追加された状態のプレハブを作成することができました!
まとめ
ScriptableWizard
はパラメータ入力に関するGUIの配置をサポートしてくれるので、エディタウインドウのGUI操作に関するコーディングを最低限で済ませることができます。その分、作成処理のコーディングへ集中することができるので、今回の例のように多少複雑な作成処理を実装したいときには有用な機能であると感じました。
これを機に今後もScriptableWizard
を使って量産作業の効率化を図っていきたいと思います。明日は@nissy_gpさんです!
- 投稿日:2020-11-30T14:23:08+09:00
LINQによるデータ操作(3)
こんにちは、Mottyです。
今回も引き続きLINQのメソッドについてまとめていきます。
ソート
LINQのメソッド式を使えば
配列やlistをソートすることができます。var source = new[] { new{Id = 1, Name = "Taro", Age = 20, Sex = "Male"}, new{Id = 2, Name = "Jiro", Age = 20, Sex = "Male"}, new{Id = 3, Name = "Saburo", Age = 10, Sex = "Male"}, new{Id = 4, Name = "Hanako", Age = 31, Sex = "Female"}, }; //OrderBy var OrderedSource = source.OrderBy( x => x.Age); //年齢でソート foreach(var item in OrderedSource) {Console.WriteLine(item);} //output //{ Id = 3, Name = Saburo, Age = 10, Sex = Male } //{ Id = 1, Name = Taro, Age = 20, Sex = Male } //{ Id = 2, Name = Jiro, Age = 20, Sex = Male } //{ Id = 4, Name = Hanako, Age = 31, Sex = Female } //OrderByDescending var OrderedSourceDis = source.OrderByDescending( x => x.Age);//年齢で降順ソート foreach(var item in OrderedSourceDis) { Console.WriteLine(item); } //output //{ Id = 4, Name = Hanako, Age = 31, Sex = Female } //{ Id = 1, Name = Taro, Age = 20, Sex = Male } //{ Id = 2, Name = Jiro, Age = 20, Sex = Male } //{ Id = 3, Name = Saburo, Age = 10, Sex = Male }Select
指定した要素を選択したり、要素に演算を加えて返したりできます。
選択var SelectedSource1 = source.Select(x => x.Age); Console.WriteLine(string.Join(",", SelectedSource1)); //20,20,10,31 var SelectedSource2 = source.Select((x,y) => new { x.Age, x.Id } ); Console.WriteLine(string.Join(",", SelectedSource2)); //{ Age = 20, Id = 1 },{ Age = 20, Id = 2 },{ Age = 10, Id = 3 },{ Age = 31, Id = 4 }演算
List<int> SourceList = new List<int> { 1, 3, 5, 7, 9 }; var SelectedSourceList = SourceList.Select(x => x + 3); Console.WriteLine(string.Join(",",SelectedSourceList)); //output: 4,6,8,10,12まとめ
第3弾ということで書くことも少なくなってきましたが、LINQは便利ですね...。
今回は匿名クラスというものを使って抽出しました。ご参考頂いたURLを載せておきます。・Selectメソッドで必要な項目だけを抜き出す。
https://yaspage.com/prog/csharp/cs-linq-select/
- 投稿日:2020-11-30T14:10:28+09:00
dotnet-5.0におけるNativeAOTについて
始めに
dotnet-5.0におけるNativeAOTについて、どういうものかという概要と、バイナリを生成するところまで書きたいと思う。
AOT(Ahead Of Time)コンパイルとは
通常dotnetのアセンブリは、ソースからILと呼ばれる中間言語に変換された形でいったん出力され、実行時にコンパイラによって、ネイティブコードに変換されて処理が実行される。これをJIT(Just In Time)コンパイルという
この変換処理を予め行い、ネイティブコードに落とし込んだバイナリを生成するのがAOT(Ahead Of Time)コンパイルとなる。特に新しい概念というわけではなく、C/C++やrust,golang等はAOTと言えると思う。
dotnetでAOTを導入する目的
そもそもdotnetではILからのJITが基本だが、それでもAOTが検討されるのは、以下の理由による。
起動時間を含む積極的な最適化
JITでは、実行時に最適化をかけるという仕組み上、起動時に著しく時間がかかるような最適化を行うことができない。
対してAOTでは、ビルド時に最適化をかけることが可能なため、時間的な制約からはある程度解放される(実用上限度はあるが)。
そのため、より深いコード解析を行うことにより、より積極的な最適化が可能になる。
また、JITという工程を飛ばすことにより、起動時間の短縮が可能となる。
起動時間の短縮というだけならば、ReadyToRunという既存のAOTで実現可能。ただし、後の最適化のためにILは温存されるので、生成物のサイズ自体は増える。ただし、JITでしかできない最適化というものはあるので、最適化でどちらが良いかというのは一概に言えるものではないというのが難しい所ではある。
また、dotnet系言語は強力なリフレクションや動的コード生成を前提にした機能や手法などが数多く存在するが、AOTとは相性が悪いため、かなり制限を受けるというのもデメリットとはいえる。サイズの削減
ビルド時に依存関係を解決することにより、不要なコードを積極的に行い、最終的な生成物のサイズを軽減できる。
大抵のアプリケーションは、ベースライブラリの全ての機能を使用しているわけではないため、この手法により、大幅なサイズ削減が見込める。ただし、基本的にILよりもネイティブコードの方がサイズが大きくなりがちなのと、C#の基礎的な機能(例外とか)を実現するためのIL→ネイティブコードの変換量がかなり大きくなるようなプラットフォーム(iOSとかWASMとか)では、却って生成物のサイズが大きくなる場合もある。
また、リフレクションを多用するようなプロジェクトでは、リフレクション用のメタ情報を持たせる必要があるため、結局コード削減量よりメタ情報追加による増加の方が勝ってしまい、生成されるもののサイズは大きくなってしまう場合がある。
プラットフォームの制限
例えばiOS等は動的コードの実行を禁止しているため、JITがそもそも行えず、AOTを行うしかないという事情がある。
NativeAOTとは
ILを完全にネイティブコードに落とし込むプロジェクトで、以前はcorertと呼ばれていた。
実はCarrionというゲームでcorert+monogameの組み合わせで使われているらしいが、あくまでcorert自体は実験的プロジェクトで、実用する際は注意が必要なものだった。それが、最近dotnet/runtimelabという、ランタイム自体に関する実験的修正を試みるプロジェクトに移って開発されることとなった。runtimelabに移ったことで、将来的にどのようにメインプロジェクトに統合されるかというのが具体的に視野に入るようになったので、前進はしていると思う。
現在の進捗を確認したい場合は、個別のブランチに移って履歴を確認することができる
NativeAOTの特徴
NativeAOTは、以下のような特徴を持つ
- 今のところサポートしてるのはWindows、Linux、macOS(x64)
- corertの時はWASM等の生成も出来たようだが、runtimelabに移ってからは記述は見つからない
- ドキュメントに載ってないだけで使えるかもしれない
- Cコンパイラが必要(C/C++のコードに一旦落としてる?)
- ネイティブアプリだけでなく、ネイティブライブラリの生成も可能
- 関数のみで、かつ引数、戻り値等に制限は付く
- 動的なコード実行に強い制限がかかる
- 複雑なリフレクションを行おうとするとエラーが出る場合がある
- 構築時に予めメタ情報を持たせておくことで、ある程度可能
- 典型的なパターンの場合は自動的にビルド時に判断して持たせてくれる
- メタデータを持たせる場合は、サイズがそれなりに増大する
- System.Reflection.Emit以下のものはできないと考えた方が良い
- 生成バイナリがプラットフォーム依存となる
- これは単一実行バイナリと同じ
メリットデメリットがかなり激しいので、採用する場合はよく考えて採用した方が良い。
NativeAOTを体験する
現状これらの制約を踏まえた上でNativeAOTを行いたい場合、ソースからビルドするというのがまず推奨されるやり方だが、
そこまでするのはきついという人も多いと思うので、ビルド済みバイナリを利用して、nuget経由で比較的手軽にできる方法を紹介する。ただし、dotnetにおけるNativeAOTはあくまでも発展途上で、大胆な仕様変更や不可解なエラーも十分あり得るため、取り扱いには細心の注意を払う事事前準備
事前に必要なものに関しては、 https://github.com/dotnet/runtimelab/blob/feature/NativeAOT/samples/prerequisites.md に記述がある。
上記に加えて、dotnet-sdk 5.0以降が必要。また、一部のlinux,mac環境では、clangコンパイラが別名表記になっている場合がある(
clang-6.0
とか後ろにバージョン表記が入る等)。
そのような環境でビルドする際は、msbuildのプロパティにCppCompilerAndLinker=[clangのパス]
を設定する必要がある。プロジェクトの作成からバイナリの生成まで
dotnet new
等で、通常のアプリケーションプロジェクトを作成する
- TargetFrameworkは"net5.0"以降、あるいは"netcoreapp*"にする
- csprojがあるディレクトリに、
dotnet new nugetconfig
等でnuget.configを作成する- nuget.configに
https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-experimental/nuget/v3/index.json
をソースとして追加するMicrosoft.DotNet.ILCompiler
のバージョン6.0.0-*
をnuget参照に追加するdotnet publish -r [RID] -c [Configuration]
を実行する
- RIDについては 公式ドキュメントを参考にすること
以上を実行すると、
bin\[Configuration]\[TargetFramework]\[RID]\native
以下に実行可能バイナリが生成される。
現状特に設定を追加せずに行うと、HelloWorldレベルのもので大体7MB程度になった。
今回は詳しく検証していないが、ビルドオプションの工夫で、もっと減らせる余地もあるらしい。ビルドオプション等は以下。
https://github.com/dotnet/runtimelab/blob/feature/NativeAOT/docs/using-nativeaot/optimizing.md今回は試してないが、ネイティブライブラリの作成も可能らしい。
終わりに
NativeAOTについて、とりあえず入り口となる部分を書いたが、今後も仕様変更等は当然起こり得る事なので、適度に動向を追っていきたい。
後、この手の技術については、dotnetだけではなく他でも色々研究されている分野なので、それらと比較していってもいいかもしれない。参考リンク
- dotnet/runtimelab
- dotnetランタイムに関わる実験場
- dotnet/runtimelabのNativeAOT関連
- tools/aot,nativeaot等に変更点がある
- NativeAOTを利用する時のガイド等
- サンプルプロジェクト
- ReadyToRun
- ReadyToRunについて
- .NET Runtime Form Factors
- .NETランタイムについての議論2020年版
- NativeAOTや、他のAOTについても触れている
- corertの使用例
- Carrion
- "いのちの輝きくんみたい"とか一部で言われてたゲーム
- ゴア描写でCERO D(17歳以上対象)なので、苦手な人は注意
- Streets of Rage 4
- 日本だと"ベア・ナックル IV"という名前になっているゲーム
- dotnet-compressor
- 圧縮、伸長ユーティリティ
- 手前味噌ながら
- corert使わないと確か100MB程度になっていたと思う
- 投稿日:2020-11-30T12:27:06+09:00
C#のネイティブ関数呼び出し(P/Invoke)時に行われていることを調べてみた
本記事はサムザップ #1 AdventCalendar 2020の12/2の記事です。
12/1の記事は@ohbashunsukeさんの【Unity】新規ゲームのUI開発で気をつけた39のTips前編 - Qiitaでした。モチベーション
P/Invokeと各種String作成ベンチマーク - Qiitaの記事で紹介されているベンチマークでは、
StringBuilderは他の手法に比べて頭一つ遅くなっていました。他と比べて遅いのはマネージド・ネイティブ間のデータの受け渡し(マーシャリング)コストが上乗せされた結果のような感じがしますが、
実際に何が行われているのか、気になったので調べてみました。準備
まずはネイティブ関数呼び出しを行う、ミニマムなコンソールアプリケーションを作成します。
$ dotnet new console
ネイティブ関数呼び出しの実装(
Program.cs
)Program.csusing System; using System.Runtime.InteropServices; using System.Text; namespace pinvoke { internal class Program { [DllImport("Kernel32", CharSet = CharSet.Unicode, EntryPoint = "GetTempPathW")] public static extern int GetTempPath(uint nBufferLength, StringBuilder sb); public static void Main(string[] args) { var sb = new StringBuilder(capacity: 260 + 1); GetTempPath(260, sb); Console.WriteLine(sb.ToString()); } } }動作確認のためにコードをビルドして実行します。
$ dotnet build $ dotnet run C:\Users\user\AppData\Local\Temp\想定通りに一時ディレクトリのパスが出力されました、ネイティブ関数呼び出しの実装は問題なさそうです。
調査
C#ではMashalAs属性を使って、ネイティブ関数呼び出し時のデータの受け渡し方法をコントロールすることができます。
設定によって文字コードの変換(ANSI → UTF-16など)もかかる、これらのマーシャリングを行うコードはいつ作られるのでしょうか。まずコンパイル時の可能性を考えますが、これはビルドしたDLLをIL DASMで見てみてもC#のコードとほぼ変わらないため、違うことが分かります。
IL DASM 出力結果
Mainメソッドからの
GetTempPath
関数呼び出しは、普通の関数呼び出しと変わらないIL_0006: newobj instance void [System.Runtime]System.Text.StringBuilder::.ctor(int32) IL_000b: stloc.0 IL_000c: ldc.i4 0x104 IL_0011: ldloc.0 IL_0012: call int32 pinvoke.Program::GetTempPath(uint32, class [System.Runtime]System.Text.StringBuilder)
GetTempPath
のメソッド本体は空になっている.method public hidebysig static pinvokeimpl("Kernel32" as "GetTempPathW" unicode winapi) int32 GetTempPath(uint32 nBufferLength, class [System.Runtime]System.Text.StringBuilder sb) cil managed preservesig { }ではコンパイル時ではないとすると、コードが生成されるのは実行時になるのでしょうか。
結論から言うとそうなります。こちらの記事1によるとランタイム(CLR)によって実行時にマーシャリングのコードが生成されること、
また、その生成されるコードの詳細をETWイベント経由で確認できるツールが紹介されています。このIL Stub Diagnosticsというツール 2 ですが、残念ながら私のPCでは動作しなかったので、
直接実行時のETWイベントから詳細を取得してみることにします。ドキュメント3を参照すると
dotnet-trace
コマンドでプロバイダーをMicrosoft-Windows-DotNETRuntime
に、
フラグを0x2000
、レベルを4
にすると、スタブ生成のETWイベントが捕捉できそうです。$ dotnet-trace collect --providers Microsoft-Windows-DotNETRuntime:0x2000:4 -- ./bin/Debug/net5.0/pinvoke.exe Provider Name Keywords Level Enabled By Microsoft-Windows-DotNETRuntime 0x0000000000002000 Informational(4) --providers Process : C:\Users\user\...\pinvoke\bin\Debug\net5.0\pinvoke.exe Output File : C:\Users\user\...\pinvoke\trace.nettrace [00:00:00:00] Recording trace 0.00 (B) Press <Enter> or <Ctrl+C> to exit... Trace completed.トレースが完了したら
trace.nettrace
というファイルが出力されますので、これをPerfViewで開いてみます。
trace.nettrace
のEventsをダブルクリックすると、新しいウィンドウでETWイベントの一覧が表示されます。今回の実装に使った
GetTempPath
でフィルタリングすると、確かにILコードがありました。ILにはあまり詳しくないので雰囲気で読み解いていくと、
スタブコードのネイティブ関数呼び出し前後で呼ばれてそうなメソッドは下記でした。コードには条件分岐も含まれていましたので正確ではありませんが、
Marshal.AllocCoTaskMem StringBuilder.InternalCopy // ネイティブ関数呼び出し StringBuilder.ReplaceBufferInternal Marshal.FreeCoTaskMemメソッド名から察するに、一時バッファを用意しそこにStringBuilderの内容をコピーし、
ネイティブ関数呼び出し後書き戻して割り当てたバッファを解放する、みたいな挙動でしょうか。なるほど他の手法に比べて遅くなりそうな雰囲気がします。
まとめ
- .NET Coreではネイテイブ関数呼び出し時に、マネージドとネイティブの橋渡し(マーシャリング)をするスタブコードを生成している
- スタブコードは生成のETWイベントにより、その詳細を知ることができる
以上になります。
明日は@phasmatodeanさんの記事です。環境
Windows 10 Pro 20H2
$ dotnet --version 5.0.100 $ dotnet-trace --version 5.0.152202+4d281c71a14e6226ab0bf0c98687db4a5c4217e3参考
- 投稿日:2020-11-30T09:36:38+09:00
.NET 5でPublishSingleFileを指定してビルドしたアプリの実行ディレクトリ
はじめに
この記事で
p:PublishSingleFile
をtrueで作ったときに気づいたのですが、.NET 5では自己完結型アプリケーションでビルドしたアプリを実行した場合に実行ディレクトリが.NET Core 3.1の時と異なる&実行ファイルパスがAssembly.GetExecutingAssembly().Location
では取得できませんでした。この記事では.NET 5でPublishSingleFileを指定してビルドした自己完結型アプリケーションで、実行ディレクトリを取得する方法を確認します。
自己完結型アプリケーションの形式でアプリをビルドして、それぞれ実行する。
.NET 5および.NET Core 3.1のコンソールアプリケーションを作成し、それぞれメイン関数に下記のコードを記載します。
Program.csclass Program { static void Main(string[] args) { Console.WriteLine($"Environment.Version={Environment.Version}"); Console.WriteLine($"Assembly.GetExecutingAssembly().Location={Assembly.GetExecutingAssembly().Location}"); Console.WriteLine($"AppContext.BaseDirectory={AppContext.BaseDirectory}"); Console.WriteLine("Hello World!"); Console.ReadLine(); } }それぞれのプロジェクトを下記のコマンドラインでアプリを自己完結型アプリケーションでビルドします。
dotnet publish -c Release -r win-x64 /p:PublishSingleFile=true /p:IncludeNativeLibrariesForSelfExtract=true実行したファイルを
c:\temp
においてからダブルクリックで実行したら下記の結果になりました。
ランタイム Assembly.GetExecutingAssembly().Location AppContext.BaseDirectory .NET 5 空 C:\temp\sample\AppUsing5.exe .NET Core 3.1 %LOCALAPPDATA%\Temp.net\AppUsingCore3\dl4pjkfu.bol\AppUsingCore3.dll C:\temp\sample\AppUsingCore3.exe .NET 5で実行ファイルのパスを求めるには?
単一ファイルの配置と実行可能ファイルにAPI の非互換性として、.NET 5で自己完結型アプリケーションでビルドした場合にAssembly.Locationでは空を返すとしっかり記載されていますね。また、
Assembly.GetFile
やAssembly.CodeBase
では例外を返すとも記載されています。実行ファイルのディレクトリを参照する場合は、AppContext.BaseDirectoryを使うのが正しいようです。
自己完結型アプリケーションの実行ディレクトリ
.NET 3.1ではZIP形式の自己完結型アプリケーションは、一度%LOCALAPPDATA%に展開された後に展開された場所からプログラムが起動されます。このため実行ディレクトリを参照する場合には
Assembly.GetExecutingAssembly().Location
などを確認する必要があったし、実行ファイルが存在するディレクトリを確認する場合はAppContext.BaseDirectory
を使うなどの使い分けが必要でした。.NET 5では一時ディレクトリに展開されず、アプリが配置された場所から直に実行されるようになります。
まとめ
.NET 5になって実行パスの取得方法がわかりやすくなりましたね。
実行ファイルのパスの取得方法について、MSのドキュメントで触れられるのは初めてなんじゃないでしょうか