UObjectをJsonにシリアライズしよう
この記事はUnreal Engine 4(UE4) Advent Calendar 2019の20日目の投稿記事です。
私がマーケットプレイスに公開しているObjectDelivererというプラグインがあります。
今回の記事はこのプラグインに実装している「オブジェクトをJsonにシリアライズする機能」について書きます。
例えばこんなVariablesを定義したブループリント(Objectを継承)があるとすると、
こんな感じのJson文字列に変換されます。
{ "IntValue":0, "BoolValue":false, "StringValue":"ABC" }
Jsonは文字列で表現されるので、そのままテキストファイルに保存することもできます。 また文字列なので人間の目で見ても中身が分かりやすいです。
このJsonをオブジェクトに復元する機能も作ることで、Jsonを介してオブジェクトの保存-復元ができるようになるのです。
ObjectDelivererではこのJson文字列を通信に乗せて送受信することで、オブジェクトを別の場所(別のアプリケーション)に届けることができています。
UE4におけるJsonの取り扱い
UE4には標準でJson
とJsonUtilities
というモジュールがあります。
Jsonモジュール
Jsonの読み書きをサポートするモジュールです。ただしブループリントでは使用できずC++のみサポートされています。
こんな感じで使えます。
Jsonオブジェクト→Json文字列
// FJsonObject(Jsonデータの入れ物)を作成 TSharedPtr<FJsonObject> jsonObject = MakeShareable(new FJsonObject()); // jsonObjectにプロパティを追加 // FStringにJsonを書き込むためのWriterを作成 FString OutputString; TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutputString); // JsonをFStringに書き込み FJsonSerializer::Serialize(jsonObject.ToSharedRef(), Writer); // OutputStringにJson文字列が入っているので何かする
Json文字列→Jsonオブジェクト
// json文字列が入っているFString FString jsonString; // FStringからJsonを読み込むためのReaderを作成 TSharedRef<TJsonReader<TCHAR>> JsonReader = TJsonReaderFactory<TCHAR>::Create(jsonString); // FJsonObject(Jsonデータの入れ物)を作成 TSharedPtr<FJsonObject> JsonObject = MakeShareable(new FJsonObject()); // Json文字列からJsonオブジェクトに読み込み FJsonSerializer::Deserialize(JsonReader, JsonObject); // jsonObjectからプロパティを取り出して何かする
Jsonオブジェクトの値のGet, Set
TSharedPtr<FJsonObject> JsonObject = MakeShareable(new FJsonObject()); // 値のセット JsonObject->SetNumberField("IntValue", 1); JsonObject->SetBoolField("BoolValue", true); JsonObject->SetStringField("StringValue", "ABCDEFG"); // 値の取得 int32 intValue = JsonObject->GetIntegerField("IntValue"); bool boolValue = JsonObject->GetBoolField("BoolValue"); FString stringValue = JsonObject->GetStringField("StringValue"); // このほかArrayやObjectプロパティもGet, Setできます
上記パターンを知っていれば、だいたいのケースで対応できると思います。
JsonUtilitiesモジュール
Jsonモジュールとは別にJsonUtilitiesモジュールというものがあります。 これはJsonモジュールを使ったサポートモジュールです。
この中にFJsonObjectConverter
というクラスがあり、名前的にはオブジェクトをJsonにできそうな気がします。
ただ中身を見ると、UStructとJSONの相互変換を提供するクラスでした。 私が実現したいのはUObjectとJSONの相互変換なので、ちょっと違いました。残念。
ちなみにこの機能を使うには以下の2つのstaticメソッドをコールすることで可能です。
- UStructToJsonObject
- JsonObjectToUStruct
UObjectとJSONの相互変換を自作する
まず自作すると書きましたが、ほぼFJsonObjectConverter
クラスの実装のままです。
FJsonObjectConverter
クラスと並べてみると分かるのですが、ほぼ一緒です。
FJsonObjectConverter
クラスの実装にUObjectのやり取り処理のみ足させていただきました。
コードは上記リンクに全部のってますが、機能を実現する上での代表的な部分のみ解説します。
UObject -> JsonObject
UObjectをJsonに変換する処理です。
まずTFieldIteratorを使ってUObjectのプロパティ(ブループリントで定義する変数も含まれる)の一覧を取得し順番に処理します。
Unreal Engineではない純粋なC++のプログラムでは、このような事(リフレクション)はできません。 (将来のC++での企画では検討されているとかの記事は見たことありますが)
UE4のC++ではリフレクションの事をプロパティシステムと呼ぶらしいです。
詳しくはここに書いてあります。 www.unrealengine.com
プロパティ情報(UProperty)が取得できると、それを使ってプロパティ名や実際のプロパティの値もとれます。
そうなるとあとは、上のほうで書いたJsonオブジェクトへのプロパティのセットと同様の方法でJsonプロパティを作ることができます。
ただしUObjectの中にUObjectのプロパティがあるような入れ子構造の場合も考える必要があるため、ここでの処理は再帰的に行われます。
TSharedPtr<FJsonObject> UObjectJsonSerializer::CreateJsonObject(const UObject* Obj) { TSharedPtr<FJsonObject> JsonObject = MakeShareable(new FJsonObject()); if (!Obj) return JsonObject; // UObjectのプロパティ一覧を順番に処理 for (TFieldIterator<UProperty> PropIt(Obj->GetClass(), EFieldIteratorFlags::IncludeSuper); PropIt; ++PropIt) { UProperty* Property = *PropIt; // プロパティ名を取得 FString PropertyName = Property->GetName(); // プロパティの値を取得 uint8* CurrentPropAddr = PropIt->ContainerPtrToValuePtr<uint8>((UObject*)Obj); FJsonObjectConverter::CustomExportCallback CustomCB; CustomCB.BindStatic(UObjectJsonSerializer::ObjectJsonCallback); // Jsonのプロパティにセットする // FJsonObjectConverter::UPropertyToJsonValueは再帰的に処理される JsonObject->SetField(PropertyName, FJsonObjectConverter::UPropertyToJsonValue(Property, CurrentPropAddr, 0, 0, &CustomCB)); } return JsonObject; }
JsonObject -> UObject
JsonからUObjectを復元する処理です。
こちらもUObjectをJsonに変換する処理と同様、UE4のプロパティシステムを使って、動的にプロパティ一覧を取得し順番に処理していきます。
UPropertyを取得し、そこからプロパティ名を取得したあとに、JsonObjectからJsonプロパティを取得してUObjectのプロパティにセットする流れです。
こちらも入れ構造に対応するために、再帰的に関数が呼ばれて行きます。
bool UObjectJsonSerializer::JsonObjectToUObject(const TSharedPtr<FJsonObject>& JsonObject, UObject* OutObject) { auto& JsonAttributes = JsonObject->Values; // 復元するUObjectのプロパティ一覧を取得 for (TFieldIterator<UProperty> PropIt(OutObject->GetClass()); PropIt; ++PropIt) { UProperty* Property = *PropIt; // JsonObjectからプロパティ名をキーにしてJsonプロパティを取得 const TSharedPtr<FJsonValue>* JsonValue = JsonAttributes.Find(Property->GetName()); if (!JsonValue) { continue; } if (JsonValue->IsValid() && !(*JsonValue)->IsNull()) { // UObjectのプロパティのポインタを取得 void* Value = Property->ContainerPtrToValuePtr<uint8>(OutObject); // Jsonプロパティから値を読み出しUObjectのプロパティにセット // JsonValueToUPropertyは再帰的に処理される if (!JsonValueToUProperty(*JsonValue, Property, Value)) { return false; } } } return true; }
いくつかの問題
ObjectDelivererでは上記ObjectJsonSerializerを使って、UObjectDeliveryBoxUsingJson内でUObjectをJsonにシリアライズしています。
この機能は自分のプロジェクトでも利用しており、一通り狙ったように動作しているのですがいくつか改善したい点がでてきました。
JsonObject -> UObject時にUObjectの型を指定する必要がある
上記JsonObject -> UObjectの処理の説明で書いたように、この時の処理はUObjectの型が事前に決まっている必要があります。
これはJsonのフォーマットを見ると分かるのですが、Jsonにはオブジェクトの型を保存しておく仕組みはありません。
そのためJsonだけ見ていても、どのUObjectに復元してよいか分からないため事前にUObjectの型を決めておく必要があるのです。
このため現在のObjectDelivererのUObjectDeliveryBoxUsingJsonを使った通信では、1つのUObjectの型のみ送受信できます。複数の型の送受信には通常の使い方では対応できません。
UObject型のプロパティをもっている場合に情報が落ちる可能性がある
これも1つ前の問題と同じく、Jsonに型情報を保存できないために発生している問題です。
例えばClassAというクラスを継承してClassBというクラスが存在する環境があるとします。
その環境でJsonに保存するUObjectにClassA型のプロパティがあります。
このClassA型のプロパティにはClassBのインスタンスを入れることができます。
この時にJsonへの保存と復元をするとClassAにはなくClassBにしかないプロパティは復元されません。。。
これはプロパティの復元時にはClassA型を示すUPropertyを使っているためです。
これは理解して使っていれば避けられますが、気づきづらいバグになってしまうかもしれません。
Jsonのプロパティ名が指定した文字列になっていないことがある
たとえば冒頭の例で示したブループリントをJsonにした時に、以下のようになってしまうことがあります。
{ "Intvalue":0, "boolValue":false, "Stringvalue":"ABC" }
一見するとちゃんとできているようにも見えますが、プロパティ名の大文字小文字が間違っています。
これはUE4のプロパティがFName型を使っているためです。
上記ページにも書かれているようにFNameは大文字小文字を区別しないため、このような現象が起きてしまいます。
Jsonへの保存と復元に両方ともObjectDelivererを使ったUE4の同一プロジェクトの場合では問題はおきませんが、他の開発環境で作られたアプリケーションとJsonのやり取りをする際には問題(プロパティ名の不一致)が起きる可能性があります。
問題の対策
実はこの記事を公開する時点で上記問題の対策を行ったアップデートを公開したかったのですが、まだ完了しておりません。。。(まだテストが終わってない)
作業は以下のブランチで行っており、実装はほぼ完了しているのでどんか感じなのかは見ることはできます。
Jsonへのシリアライズ部分はだいぶ変更を入れています。
https://github.com/ayumax/ObjectDeliverer/tree/Remodeling-ObjectJsonSerializer
行った対策は以下の2つです。
Jsonの中にUObjectの型情報をユーザー選択式で書き込む
問題の1つ目と2つ目は、Jsonの中にUObjectの型情報を書き込んでおけば解消されます。
ただしそれを行うと以前とはJsonのフォーマットが変わり互換性が失われることや、純粋にJsonの文字列数が増えるためデータサイズも大きくなり、ObjectDelivererの主目的であるデータの送受信という観点からは好ましくない場合もあります。
そこでUObjectの型毎にJsonの中に型情報を書き込むか書き込まないかを使う人が選択できる仕組みを導入します。
これにより送受信する型は1つだけで、クラスの継承を使ったUObjectのプロパティは作らないという条件なら以前と同じJsonを作成することもできます。
また実際のインスタンスの型情報を書き込む事を選択することで、UObjectの保存、復元を行ったときに確実に復元させることもできます。
具体的には作成されるJsonは以下のような違いが生まれます。
型情報を書きこまない場合(以前までの実装)
{ "IntValue":0, "BoolValue":false, "StringValue":"ABC" }
型情報を書き込む場合
例としてNewBlueprint
という名前のUObjectをシリアライズした場合
{ "Type":"NewBlueprint", "Body" : { "IntValue":0, "BoolValue" : false, "StringValue" : "ABC" } }
プロパティ名の変換関数を実装するインターフェイスの定義
FName型からFString型への変換は自動に任せると狙った文字列にならない可能性があるため、ユーザーが自分で確実に定義できるようプロパティ名変換を行うインターフェイスを追加しました。
シリアライズ対象のUObjectにこのインターフェイスを実装すると、自前のプロパティ名変換関数を使ってJsonプロパティの書き込み、読み込みを行うようになります。
インターフェイスが実装されていない場合は従来通り自動でFName -> FString変換が行われます。
この機能を使うことで、全く違う文字列でJsonプロパティを作ることもできるようになるので、今回の問題解決以外にも使い道はあると思っています。
最後に
今回はUObjectをJsonObjectにシリアライズする話を書きました。
今回記事の中に書きましたJsonUtilitiesモジュールのFJsonObjectConverterのソースはJson以外へのUObjectのシリアライズを作りたい場合や、UE4のプロパティシステムを利用する上でも参考になる実装が含まれているため是非一度見てみてください。
また私の公開していますObjectDelivererはブループリントのみでTCP/IPやUDPなどを使った通信を簡単に作ることを目指して作っています。価格Freeで公開しているのでよろしければお試しください。
.NET Coreでアセンブリをアンロードする
アセンブリをアンロードする仕組み
この記事は C# Advent Calendar 2019 の10日目です。
私がお仕事で作っているアプリケーション(.NET Framework製)にユーザープラグイン(DLL)読み込み機能を持つものがあります。
これは利用者がルールに則ったDLLを自分で作ることで、開発ツールに機能を足すことのできる機能です。
この機能は開発ツール側でいったん該当のDLL達をロードし問い合わせを行い、読み込む必要のないDLLはアンロードする処理が入っています。
この開発ツールを.NET Coreに移植する際に.NET Frameworkとは異なる手法を使う必要があったので紹介します。
AppDomainは使えない
.NET Frameworkではアセンブリのアンロードを行うにはAppDomainを使いました。
アプリケーション規定のAppDomainの他にドメインを生成し、その中でアセンブリをロードします。
インターフェイスなどを経由してアセンブリの機能を利用し終わったら、AppDomainのUnloadメソッドをコールしてドメインごとアセンブリをアンロードします。
ただし.NET CoreではAppDomainは1つしかサポートされていないため、この方法は使えません。
その代わり.NET Coreではアセンブリのアンロードを行うために AssemblyLoadContextが用意されているので、今回はこれを使ってみます。
AssemblyLoadContextとは
AssemblyLoadContextとはAppDomainと同様にアセンブリ読み込みを閉じたスコープ内で行えるものです。
この中にロードしたアセンブリは、AssemblyLoadContextのUnloadメソッドをコールすることでまとめてアンロードできますが注意することがあります。
注意点1 isCollectibleをtrueにする
AssemblyLoadContextのコンストラクタ引数にあるisCollectibleはtrueにする必要があります。
これはパフォーマンスの観点からデフォルトではfalseになっているためです。
falseのままUnloadをコールすると例外が発生します。
System.InvalidOperationException HResult=0x80131509 Message=Cannot unload non-collectible AssemblyLoadContext. Source=System.Private.CoreLib
注意点2 アンロードは強制的ではなく協調的である
AssemblyLoadContextのUnloadメソッドはコールされた時点では開始されるだけで、完了していません。
アンロードが完了されるのは、以下の条件を満たしたときです。
- コール スタックに、AssemblyLoadContext にロードされたアセンブリ内のメソッドを含むスレッドがなくなった。
- AssemblyLoadContext にロードされたアセンブリ内の型、それらの型のインスタンス、およびアセンブリ自体が参照されなくなった。
つまりちゃんと考えて設計をしないと、いつまでたってもアンロードが完了しない事も起こります。
サンプル作成
それでは実際にアセンブリをアンロードする仕組みを実装してみます。
1. ロードされるクラスライブラリを作成
なんでもよいのですが、以下のような簡単なクラスを定義しました。 TargetFrameworkにはnetstandard2.1を指定しています。 これはClassLibrary1.dllという名前で出力されるようにしておきます。
namespace ClassLibrary1 { public class Class1 { public void Hello(int arg) { Console.WriteLine(arg); } } }
2. ロードする側にAssemblyLoadContextを実装
まずはアセンブリを読み込むためのAssemblyLoadContext を作成します。 今回作成したのは以下のような簡単なクラスです。
重要なのはコンストラクタにてisCollectibleをtrueにしている部分です。 このフラグをtrueにすることでアンロードがサポートされます。
class TestAssemblyLoadContext : AssemblyLoadContext { public TestAssemblyLoadContext() : base(isCollectible: true) { } }
3. DLLのロード&アンロードする仕組みの実装
先程作ったClassLibrary1.dllをロードして関数をコールし、アンロードするための関数を作ります。
[MethodImpl(MethodImplOptions.NoInlining)] static void ExecuteAndUnload(string assemblyPath, out WeakReference alcWeakRef) { // アセンブリをロードするAssemblyLoadContextを作成 var alc = new TestAssemblyLoadContext(); // アセンブリをロード Assembly a = alc.LoadFromAssemblyPath(assemblyPath); // 外からアンロードを検知するために弱参照を設定 alcWeakRef = new WeakReference(alc, trackResurrection: true); // リフレクションで関数コール var type = a.GetType("ClassLibrary1.Class1"); var instance = Activator.CreateInstance(type); var helloMethod = type.GetMethod("Hello"); helloMethod.Invoke(instance, new object[] { 1 }); // アンロード実施 alc.Unload(); }
作成する関数には念のためMethodImplOptions.NoInliningをつけておきます。
これはこのExecuteAndUnload関数がインライン化されないようにするためです。
AssemblyLoadContextを使ったアンロードはAssemblyLoadContext内の型やインスタンスが外部から参照されていると実施できません。
そのためインライン化しているとExecuteAndUnloadの呼び出し元(今回ではMain関数)に参照が残る恐れがあります。
(※ ただし今回の作成サンプルではMethodImplOptions.AggressiveInliningにしてもアンロードは完了できました)
[MethodImpl(MethodImplOptions.NoInlining)] static void ExecuteAndUnload(string assemblyPath, out WeakReference alcWeakRef)
AssemblyLoadContextのインスタンスを作成し、その中にアセンブリをロードします。
assemblyPathにはdll or exeのファイルパスが入ります。
// アセンブリをロードするAssemblyLoadContextを作成 var alc = new TestAssemblyLoadContext(); // アセンブリをロード Assembly a = alc.LoadFromAssemblyPath(assemblyPath);
AssemblyLoadContextの参照を弱参照として残します。
この参照はExecuteAndUnloadの呼び出し元でアンロードが完了しているかを判断するのに使います。
// 外からアンロードを検知するために弱参照を設定 alcWeakRef = new WeakReference(alc, trackResurrection: true);
今回は実験のためクラス名を指定してインスタンスを作成し、リフレクションで関数を呼びました。
ここはプラグインを作る際は別アセンブリに定義したインターフェイスを実装するクラスをClassLibrary1.dllに作り、インターフェイス経由で呼び出したほうが実用的ですね。
// リフレクションで関数コール var type = a.GetType("ClassLibrary1.Class1"); var instance = Activator.CreateInstance(type); var helloMethod = type.GetMethod("Hello"); helloMethod.Invoke(instance, new object[] { 1 });
Unloadメソッドをコールしてアンロードを実施します。
// アンロード実施
alc.Unload();
ExecuteAndUnloadを使ってアセンブリをアンロードしてみる
以下のようにMain関数を実装して実際に、アセンブリを動的ロードしたあとアンロードしてみました。
今回はアンロードが成功しているかの判断は、dllファイルを消せるかどうかで調べてみました。 アンロードが完了していないと、Deleteに失敗します。
static void Main(string[] args) { var myDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); // 読み込むアセンブリ(dll)のパス var assemblyPath = Path.Combine(myDirectory, @"..\..\..\..\ClassLibrary1\bin\Debug\netstandard2.1\ClassLibrary1.dll"); // アセンブリを読み込んで関数をコール ExecuteAndUnload(assemblyPath, out WeakReference alcWeakRef); try { File.Delete(assemblyPath); } catch(UnauthorizedAccessException) { Console.WriteLine("アンロード完了してないので消せない"); } // アンロードされるまで待つ int counter = 0; for (counter = 0; alcWeakRef.IsAlive && (counter < 10); counter++) { GC.Collect(); GC.WaitForPendingFinalizers(); } if (counter < 10) { // この段階ではアンロード済みなので消せる File.Delete(assemblyPath); Console.WriteLine("アンロード成功"); } else { Console.WriteLine("アンロード失敗"); } Console.ReadKey(); }
ExecuteAndUnloadをコールして、アセンブリのロード→アセンブリ内関数実行→アンロードを行った後、WeakReferenceを使ってアンロードの完了をチェックしています。
アンロード対象のAssemblyLoadContextの参照を無くすため、GC.Collect()で強制的にガベージ コレクションを実行しています。
今回試した結果では、うまくアンロードできているときは、ループを抜けた時にcounterの値は2になっていました。
またExecuteAndUnloadを抜けた直後ではdllの削除に必ず失敗しますが、アンロード完了を待った後では削除成功していることからもアセンブリのアンロードがうまくいっていることが確認できました。
いじわるをしてみる
上記のサンプルはアンロードが成功するケースでしたが、ではどのような記述をすると失敗するのかを少し試してみました。
読み込んだアセンブリの中のスレッドが終わらないケース
ClassLibrary1.dllのHelloメソッドの中身を以下のように変更してみました。
一度Helloが呼ばれると、Task.Runで終わらないスレッドが生成されます。
public void Hello(int arg) { Task.Run(() => { while(true) { Thread.Sleep(1); } }); Console.WriteLine(arg); }
このアセンブリをロードしてアンロードを試みると失敗しました。
AssemblyLoadContextを保持してみる
アセンブリをロードする側のクラスに以下のようなstaticな変数を作って、
private static AssemblyLoadContext assemblyLoadContext;
ExecuteAndUnloadの中でTestAssemblyLoadContextの参照を保持してみました。
var alc = new TestAssemblyLoadContext();
assemblyLoadContext = alc;
このケースもAssemblyLoadContextへの参照が残っているため、アンロードには失敗しました。
ロードするアセンブリ内のTypeを保持してみる
アセンブリをロードする側のクラスに以下のようなstaticな変数を作って、
private static Type class1Type;
ExecuteAndUnloadの中でClassLibrary1.Class1のTypeを保持してみました。
var type = a.GetType("ClassLibrary1.Class1");
class1Type = type;
このケースもTypeへの参照が残っているため、アンロードには失敗しました。
まとめ
.NET Coreの環境でもAssemblyLoadContextを使ってアセンブリのロード&アンロードをすることができました。
ただしアンロードするアセンブリの後始末を忘れるとアンロードに失敗するので、ロードからアンロードまでの処理はなるべく閉じエリアで行うのがよさそうです。
実際問題アセンブリのアンロードが必要なケースはそこまで多くはないと思いますが、知っておくとどこかで役に立つかも。
Sample Project
今回作成したサンプルは以下のリポジトリにあります。
参考サイト
ObjectDeliverer Ver 1.4.0リリース 送信元IPアドレスの取得機能追加
ObjectDeliverer Ver 1.4.0
数日前の話ですがObjectDelivererの新バージョンをリリースしました。 (ブログ書こうと思って忘れてた)
送信元IPアドレスの取得機能追加
通信プロトコルをTCP/IP Server もしくはUDP Receiverにしている場合にのみ使用可能な機能です。
これら2つの通信プロトコルでは、送信元が複数のIPアドレスの可能性があります。
ただし以前のバージョンまではどこからの送信だったのかを知るすべがありませんでした。 (TCP/IP Serverには厳密には送信元を区別して認識する機能はありましたが、IPアドレスを知る機能はありませんでした)
これはObjectDelivererの設計思想の「異なる通信プロトコルを同じように切り替えて使える」を実現するために、あまり個別のプロトコルに特化した機能は入れたくなかったというのがあります。
ただ現実的にないと困ることもあり、今回実装しました。
以下仕様例です。
ReceiveDataイベントのClientSocketオブジェクトをGetIPV4Infoインターフェイスを通して使うことで情報の取得ができます。
今回追加したGetIPV4InfoインターフェイスにはIPアドレスをuint8の要素数4の配列で取得するGetIPAddressと"..."の文字列形式で取得するGetIPAddressinStringがあります。
対応している通信プロトコル以外ではReceiveDataイベントのClientSocketオブジェクトがGetIPV4Infoインターフェイスを実装していないため、インターフェイスへのキャストで失敗します。
今後もなるべく同じように使えるというコンセプトは崩したくないですが、個別プロトコル固有の機能はインターフェイス経由で使えるようにしていこうと思います。
.NET Conf 2019 meetup in AICHIでLTしてきました
.NET Conf 2019 meetup in AICHI
10/5に行われた ".NET Conf 2019" のローカルイベントです。 今回はそれに参加してLTさせて頂いてきました。
発表内容
今回は.NET Core 3.0のイベントのため、Core 3に絡んだ内容の方がいいよねっと思い、.NET Core 3.0で追加されたWPFについてお話させていただきました。
今回発表した内容の資料は以下になります。
www.slideshare.net
あと当日の発表内容を動画にしていただけました。 @kekyo2さんありがとうございます。
自分の発表の反省をしようと思い、公開していただいた動画を見てみたのですが、自分のしゃべっている動画を見るのってメチャクチャ恥ずかしいですね!!
あまりの恥ずかしさにまだ全部見れていません。
また後日気持ちを整えて見たいと思います。
テーマ選定について
自分はそこまで.NETの深いところの話ができるほど知識がないので、言語仕様などを語るよりは実践した体験を展開するような方式にしようと思ってました。
それでどうしようかと考えていて、今年のゴールデンウィーク当たりに.NET Core 3のプレビューを使ってWPFを試していたのを思い出し、ちょうどいい発表材料になるな!と思いテーマはWPFにしました。
.NET Core 3.0のWPFを試している進捗です。
— ayuma (@ayuma_x) 2019年5月6日
HoudiniみたいなノードベースUIを作成中。なかなか面白いのでこのまま作りこむ予定。#WPF #dotnetcore pic.twitter.com/C9TNKZOUHS
そこでこのテーマで話す内容をまずは箇条書きで書き出してみて、資料作成の土台を作っていきました。
そうするとある1つの重大なことに気づきました。
面白くない。。。
今回.NET Core 3.0でWPFが使えるようになった事をツラツラと書いてみたのですが、中身が薄く面白くありません。
ここで「テーマまずかったかな??」、「同時に試してたC#8の話の方が書くことあるかな??」と悩んだのですが、やはり普段から使っているWPFの方がなんかあった時も答えられるしここは崩さず行こうと決めました。
そこでWPF固有の話ではないですが、関連度が高そうなRuntimeをアプリケーションに含められる話や単一実行ファイルを作成できる話を追加して、あとその様子を動画にして実際にどんな感じで動くのかを見てもらえる構成にしました。
全部書いてみて、発表の練習を数回してみたところ、だいたい13~14分くらいになったのでこれで行くことにしました。
まだ少し内容薄いような気もしたけど、今更追加できんし腹をくくろうと。 (イベント終了してから数日経った今ならもう少しCore3 + WPFでネタができました。まあそんなもんです)
発表当日
自分の順番は終盤で、それまでは他の登壇される方の話を興味深く聞かせていただきました。
@kekyo2さんの非同期の話は、ちょうどこの日のちょっと前に昔書いたコードのTask.Wait撲滅活動をしていたこともあり個人的にHitしました。
非同期イテレーター凄く関心があるので、是非近いうちに書いてみよーっと。
私は発表は昨年のLTに続き2回目だったのですが、今日は前回よりは気持ち的に落ち着いていて「今日はテンパらないかなー」と楽観視してました。
ところが自分の番はちょうど休憩後で少し待ち時間があり、PCの準備をして時間まで待っていると段々緊張してきてお腹痛くなってきましたwww
でも今日は「早口にならずゆっくりしゃべる」「聞いてくれる人の顔を見て話す」というのを目標にしていたのでそれだけは守れるように、なるべく落ち着いてちょっとずつしゃべりました。
ただアクシデントもあり、自分の当日持って行ったMacBookの映像出力が調子悪く何度かプロジェクターが砂嵐になってしまい、若干焦りましたが結果的に時間も丁度よいくらいで終われたので良かったです。
自己評価ですが前回よりは上手く発表できたと思います!
発表終わったらお腹の痛みも消えましたw
次への反省
他の方の発表を聞いていて、どなたも自分の話す内容は深く理解されているなあと感じました。
自分も話す内容については、調べて試してやっていたのですが、完ぺきな自信があって話しているかというと、そうでもない所もあり、本当はもう少しネタは多かったのですがそういった自信のない部分はカットしたりもしてます。
やはり自信をもった発表をするには、普段の技術に裏付けされた膨大な知識が必要なのだなあと感じました。
今まで過ごしてきた時間は変えることができないので、少なくとも今後は学習時に今までよりも1歩深く内容を調べて自信をもって他人に話せる知識を持ちたいなあと感じます。
今回もまた学べる事が多く、LTを終えることで自分の自信にもなったので参加できて良かったなと感じました。
おまけ
会の終了後にじゃんけん大会があり、なんとmatsujirushiさんよりKinect V1を頂いてしまいました!!!
早速試しましたが楽しい!モーションキャプチャーは初体験だったので夢が広がります。
現在これで何を作ろうか検討中です。
Kinect V1を動かせた。
— ayuma (@ayuma_x) 2019年10月7日
何これ!控えめに言って面白い! pic.twitter.com/RzjbdSIXbR
XPS 15 2-in-1レビュー(デルアンバサダープログラム)
デルアンバサダープログラムに当選した
先日Twitterを見ていたらデルアンバサダープログラムでノートPCを借りてレビューしている人をみて、試しに自分も登録して応募してみました。
そうすると、なんと当選してしまいXPS 15 2-in-1をお借りする機会をもらったためレビューさせていただきます。
XPS 15 2-in-1をお絵かきなどで使っているレビューはきっと他のかたがたくさんしていると思うので、私はソフトウェア開発者目線でレビューしてみたいと思います。
主に今使っているDELL ALIENWARE13 R3と比較して感想を書きたいと思います。
サイズの比較
ALIENWARE13とサイズを比較してみました。
ALIENWAREは13インチとしては大きいほうなのもあり、ほとんどサイズ的には同じくらいですね。
それだけXPSは15インチとしてはスリムにできているのだと思います。
またディスプレイもベゼルレスでとても大きく感じ見やすいです。
また個人的には持ち運び時にはACアダプタも持っていくので、アダプタの大きさ重さも気になります。
ところがアダプタはXPSの方が断然小さく、重さも軽いです。 ALIENWAREはGeForce 1060積んでいるのでしょうがないんですけどね。
アダプタのコネクタがUSB-TypeCなのも、互換性が高くてよいですね。
勉強会などで外にノートを持っていく機会も多いので、この持ち運びの良さは凄いメリットだなあと感じました。
スペック
入っているOSはWindows10のHomeで、CPUはCore i7-8705G, メモリは16GBです。
効いたことないCPUの型番だなあと思って調べたら、 Radeon™ RX Vega M GL とくっついてるCPUなんですね。
インテル HD グラフィックスよりもハイパワーなGPUが使えるという事で、凄い期待が高まります。
ディスプレイは4Kで鮮やかな色です。
今使っているALIENWAREはFullHDの13inchですので、やはり大きく感じますね。 あと4Kなので小さい文字もくっきり読めていい感じです。
スケール100%で使うと文字が小さすぎるので、175%で使ってみました。
Unreal Engineを使ってみる
Unreal Engine(UE4)はゲームをつくることのできるゲームエンジンなのですが、これを使った開発には結構マシンパワーが必要です。
外部GPUを使ってないノートだと、初期状態で立ち上げただけで重くてカクついてしまうのでXPSではどうなるかなと思って確認してみました。
結果UE4のEditorはあっさりと立ち上がり、初期状態でPlayしたところフレームレートも90fps程度でています。
2018年モデルのMacBookAirでは20fpsくらいだったので、それよりも全然快適です。
これは外に持って行ってちょっとした修正程度なら全然使えるなと思いました。
Radeon™ RX Vega M GLはスペックを見ると、自分のALIENWAREのGeForce 1060よりもだいぶ処理能力は低いような印象を持ちましたが実際に使ってみると意外に頑張るなと、良い意味で期待を裏切られました。
ただしマシンの処理的には結構頑張ることもありUE4を立ち上げるとファンが高速に回転しだし、音も結構聞こえます。
Visual Studioを使ってみる
続いてVisual Studioを使ってみました。
自分の公開しているUE4向けのC++ プラグインのコードを書いてみましたが、これはCPUも結構早いのを積んでいるので特にストレスなく書けました。
また画面が広いのでVisual Studioの使い勝手は13inchのALIENWAREよりも圧倒的に良いです。やはりコードを書くのは広い画面の方がよいですね。
1点だけ個人的に残念だったのはキーボードです。
これは個人的な好みかもしれませんが、カチャカチャしたキーボードは安っぽく感じてしまい好きになれませんでした。
ALIENWAREみたいな打ちごごちだったら、完ぺきだったのになーと思いましたが筐体の厚みもかなり薄くできているのでしょうがないかもしれません。
まとめ
借りれていた期間は1か月程度ですが、あっという間に終わってしまいました。
XPSはその他のメーカーのノートと比較しても、見た目もスタイリッシュでカッコよく憧れていたので使う機会がもてて楽しかったです。
私はUE4の開発しているので、PCにGPUが絶対必須なのですがこれだとスリムなボディに関わらずそこそこ動くGPU積んでいるのでアリだなあと思いました。
また次のPCを購入するときには選択肢が広がってしまって悩みそうですwww
デルアンバサダープログラムおすすめです。(ALIENWAREも借りてみたいなーーー)
UE4のスクリプトをC#で書きたい(実験編)
C#でスクリプトを書きたい
私はUE4のC++は好きです。マーケットプレイスにもいくつかC++プラグインを出してます。ですがC#もたくさん書いているので、時々C#だったらもっと楽に書けるのになあと思うことはあります。
そんな事を考えていて、ふと次のような書き込みをTwitterでしました。
ふとUE4のスクリプトをC#で書きたいなあ。どうしたらいいかなあと考えていたんだけど、なかなかいい実装が思いつかない。
— ayuma (@ayuma_x) 2019年8月24日
すると@kekyo2さんからこんなお誘いがあり、今日実際に1日かけてトライしてきました。
IL2C
今回の主役となるツールは@kekyo2さんの作成されているIL2Cです。
IL2Cを用いるとC#をコンパイルしたILを含むDLLやEXEからC言語のソースコードが生成できます。( 凄い)
これを使ってC#のコードをUE4のC++から呼んでみようというのが今日行った実験です。
開発環境
今回行った実験は以下の環境でおこなっています。
- Visual Studio 2019
- Unreal Engine4 Ver. 4.22
また、以下に今回行った手順を書いていきますが色々試行錯誤した結果のため、手順を見ただけでは何故それをやるのかわからない箇所もあるかと思います。
今回の試みはまだ実験段階のためご了承ください。
今回作成したプロジェクト一式は以下に置いてあります。(UE4, C#全部入ってます) github.com
C#プロジェクトの作成
まずC#のクラスライブラリのプロジェクトを作成します。今回は.NET Standardのクラスライブラリを選択しました。
次にプロジェクトファイル(*.csproj)のPropertyGroupに
この記述は通常.cで出力されるファイルを.cppで出力します。
これはUE4でこの後ビルドを正常に通すために必要な設定です。
<PropertyGroup> <TargetFramework>netcoreapp2.1</TargetFramework> <IL2CEnableCpp>true</IL2CEnableCpp> </PropertyGroup>
次にデフォルトで作成されているClass1.csに以下の記述を行います。
using System; namespace UE4Il2CSample { public class Class1 { public static int Add(int a, int b) => a + b; } }
staticなメソッドを1個用意しました。 単純にint型の引数2つを足し算する関数です。
今回はこのAdd関数をUE4から使ってみる事を目標にします。
次にNuGetから「IL2C.Buid」をプロジェクトに追加します。
追加が完了したらビルドを行います。
C#プロジェクト側でやることは以上です。簡単!!
Cのコードの確認
C#プロジェクトでのビルドが成功すると、[*.dllの出力ディレクトリ]-[IL2C]に変換されたCのコード一式ができています。
この出力されたCコード一式をUE4側で使っていきます。
UE4プロジェクトの作成
C#を変換したCのコードを使うUE4のプロジェクトを使います。 どんな設定でも構わないですが、今回はC++コードを使うのでC++プロジェクトでIL2CTestという名前のプロジェクトを作成しました。
(ブループリントプロジェクトで作成しても問題はないです)
IL2Cランタイムのビルド
C#から出力されたCコードを実行するために必要なランタイムを追加します。
今現在は自分でランタイムをビルドする必要があります。
まず以下のnugetのページから手動でパッケージをダウンロードします。
NuGet Gallery | IL2C.Runtime 0.4.70
次にダウンロードしたil2c.runtime.0.4.70.nupkgをil2c.runtime.0.4.70.zipにリネームしたあと解凍します。
解凍したディレクトリのil2c.runtime.0.4.70\lib\native\ディレクトリをUE4のプロジェクトディレクトリにコピーします。
nativeディレクトリ内にC++のスタティックライブラリのプロジェクトを新規作成し、中のsrcとincludeをプロジェクトに追加します。
次にプロジェクトのプロパティから[構成プロパティ]-[C/C++]-[全般]-[追加のインクルードディレクトリ]に以下のディレクトリパスを追加します。 $(ProjectDir)/include
ここでIL2CTest\native\src\Core\il2c_allocator.cの中身を一部修正します。
void* il2c_cleanup_at_return__(void* pReference) { IL2C_THREAD_CONTEXT* pThreadContext = il2c_get_tls_value(g_TlsIndex__); if (pThreadContext != NULL) { pThreadContext->pTemporaryReferenceAnchor = pReference; } return pReference; }
設定が完了したらRelease, x64でビルドします。
native\x64\Release\にIL2C.Runtime.libができているのを確認します。
UE4プロジェクトへ変換されたCファイルを追加
以下のディレクトリにC#から出力されたincludeとsrcディレクトリをコピーします。
IL2CTest(UE4のプロジェクトディレクトリ)\Source\IL2CTest
これでUE4のC++プロジェクトに必要なファイルが追加されました。
ちょっとしたおまじない
現状ビルドを通すために以下のおまじないをします。
- Class1.cppと同じディレクトリにClass1.hを追加します。(中身は空でよい)
Class1.cppの先頭行に
#include "Class1.h"
を追加します。[C#ライブラリ名]_internal.cppの先頭行にも
#include "[C#ライブラリ名]_internal.h"
を追加します。
このおまじないをしないと現状ビルドを通すことができないです。
プロジェクトの更新
IL2CTest.uprojectを右クリックし、「Generate Visual Studio project files」を選択し、プロジェクトファイルを更新します。
以下のように必要なファイル一覧がプロジェクトに並びます。
Cファイルへのインクルードパスの追加とlibファイルの参照追加
追加したCのファイルとランタイムへのインクルードパスをビルド設定ファイル(IL2CTest.Build.cs)に記述します。
また合わせてランタイムライブラリへの参照設定も記述します。
// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. using UnrealBuildTool; public class IL2CTest : ModuleRules { public IL2CTest(ReadOnlyTargetRules Target) : base(Target) { PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "HeadMountedDisplay" }); PublicIncludePaths.AddRange(new string[] { // IL2C.Runtimeのincludeディレクトリの追加 ModuleDirectory + "/../../native/src/", ModuleDirectory + "/../../native/include/", // C#から生成されたCファイルのincludeディレクトリの追加 ModuleDirectory + "/src/", ModuleDirectory + "/include/" }); // IL2C.Runtime.libの参照追加 PublicAdditionalLibraries.Add(ModuleDirectory + "/../../native/x64/Release/IL2C.Runtime.lib"); } }
UE4プロジェクトのビルド
以上で設定は完了したのでビルドします。
設定がうまくいっていれば以下のように成功するはずです。
UE4 C++からCの関数を呼ぶ
実際にC#から変換されて出力されたCの関数を呼んでみます。
今回はActorを継承したIL2CSampleActorを作成し、BeginPlayの中から呼んでみました。
// Fill out your copyright notice in the Description page of Project Settings. #include "IL2CSampleActor.h" #include "include/UE4Il2CSample.h" // Sets default values AIL2CSampleActor::AIL2CSampleActor() { // Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it. PrimaryActorTick.bCanEverTick = true; } // Called when the game starts or when spawned void AIL2CSampleActor::BeginPlay() { Super::BeginPlay(); auto addRet = UE4Il2CSample_Class1_Add__System_Int32_System_Int32(1, 2); UE_LOG(LogTemp, Log, TEXT("IL2C add ret = %d"), addRet); }
上記cpp内のUE4Il2CSample_Class1_Add__System_Int32_System_Int32
がC#で作成したAdd関数です。
関数名が長いのは関数名をユニークにするために、ネームスペース、クラス名、関数名、引数の型(オーバーロード対策)が繋がっているからだそうです。
今回は1 + 2を計算しているのでうまくいけば3が返ってくることになります。
実行
先程作成したIL2CSampleActorをレベルに配置してPlayしてみます。
ログからちゃんと足し算されている結果が確認できました。
まとめ
@kekyo2さんに丸1日付き合っていただき、C#で書いた関数をUE4のC++から呼ぶことができました。 今後色々探っていく中でこの仕組みの可能性を探っていけたらいいなあと思ってます。
次はIL2C.RuntimeをUE4のプラグインにすると、UE4側での実装量がだいぶ減るような気がするのでチャレンジしてみようかと思います。
UE4で作ったiOSアプリのディレクトリを標準ファイルアプリで共有する
UE4で作成したiOSアプリのパッケージの中身のディレクトリを、iOS標準のファイルアプリで確認するための方法です。
Project SettingsのiOSで以下の2つを実施します。
- File System - Supports ITunes File Sharing
チェックON
- Extra PList Data- Additional Plist Data
以下を入力
<key>LSSupportsOpeningDocumentsInPlace</key>\n<true/>\n
上記設定で作成したアプリをiOS端末に入れると、標準ファイルアプリの「このiPad内」からアプリの中身が確認できます。
この方法を使うと他のアプリからUE4で作成したアプリにファイルを渡せそうなので使い道がありそうです。