tmytのらくがき

個人の日記レベルです

WPFの子ウィンドウとしてUnityを実行してみる

UnityってWPFの中に表示したりできひんよね?って聞かれたからドキュメント読んだらできるって書いてたからやってみたらできました。という話。

TL;DR

  • -parentHWND 0x**** で子ウィンドウにできる
  • HwndHostでUnityのプロセスを子ウィンドウとして起動するだけ

Unityを子ウィンドウで起動する

Windows向けにビルドしたUnityはコマンドライン引数で -parentHWND を付けて起動するとUnity Playerは子ウィンドウとして起動します。その時の画面幅などはSTARTUPINFOで指定されたものを使用します。と、Unityのドキュメントに書いてあります。

Windows スタンドアロンアプリケーションを別のアプリケーションに埋め込みます。これを使用する場合は、親アプリケーションのウィンドウハンドル (HWND) を Windows スタンドアロンアプリケーションに渡す必要があります。

これ、日本語だと特に書いてないんですけど、0xXXXXの形で16進数として渡すと成功します。 追記: 0xなしで10進でもOKでした

簡単にC#から起動するにはこんな感じ。

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        Loaded += MainWindow_Loaded;
    }

    private void MainWindow_Loaded(object sender, RoutedEventArgs e)
    {
        StartUnity();
    }

    void StartUnity()
    {
        var apppath = @"C:\doko\soko\unity-app.exe";
        var cmdline = $"-parentHWND {new WindowInteropHelper(this).Handle}";
        Process.Start(apppath, cmdline);
    }
}

実行すると、UnityがWPFの中で動いているような感じになりました。が、リサイズしてもウィンドウサイズが追従しないのでいまいちです。

f:id:tmyt:20190926065827p:plain

HwndHostをそれとなく実装していく

HwndHostを実装すれば子ウィンドウのサイズをWPFが面倒見てくれるようになります。Unityを子ウィンドウとして起動してくれるHwndHostを実装したクラスを書いてリサイズをいい感じに面倒見てもらいます。

class UnityHost : HwndHost
{
    private Process _childProcess;
    private HandleRef _childHandleRef;

    public string AppPath { get; set; }
    public HandleRef Child => _childHandleRef;

    protected override HandleRef BuildWindowCore(HandleRef hwndParent)
    {
        var cmdline = $"-parentHWND {hwndParent.Handle}";
        _childProcess = Process.Start(AppPath, cmdline);
        while (true)
        {
            var hwndChild = User32.FindWindowEx(hwndParent.Handle, IntPtr.Zero, null, null);
            if (hwndChild != IntPtr.Zero)
            {
                return (_childHandleRef = new HandleRef(this, hwndChild));
            }
            Thread.Sleep(100);
        }
    }

    protected override void DestroyWindowCore(HandleRef hwnd)
    {
        _childProcess.Dispose();
    }
}

static class User32
{
    [DllImport("user32.dll")]
    public static extern IntPtr FindWindowEx(IntPtr hParent, IntPtr hChildAfter, string pClassName, string pWindowName);
}

急にコードが増えた気がしますが気のせいです。BuildWindowCoreの中でUnityを起動して、できたウィンドウのハンドルを返してるだけです。 このままだと、起動するパスがどこか明示されていないのでXAMLから指定します。

<Window x:Class="WpfApp4.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:WpfApp4"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <local:UnityHost AppPath="C:\doko\soko\unity-app.exe" />
    </Grid>
</Window>

これでリサイズできるようになりました。

起動時にWPFのクライアントエリア全体にUnityが出ないようにする

ここまでのコードだと、XAMLでUnityHostをGridにいれてサイズを変えたりしていても、Unityを起動したときにクライアントエリア全体にUnityが一瞬表示されます。

f:id:tmyt:20190926070602p:plain

これを回避するにはUnityのドキュメントに書いてあったようにSTARTUPINFOを指定すればいいんですが、残念ながらそれはP/Invokeです。ということでP/Invoke使っていろいろ実装したものがこちらです。

gist.github.com

P/Invokeのせいでやたらと長くなった…やな感じ… 実際の処理はBuildWindowCoreを参照してください。STASRTUPINFO構造体を埋めて、CreateProcessを呼び出しているだけです。

おしまい

  • Unityプレイヤーを-parentHWNDコマンドライン引数を使って、WPFに埋め込んでみました。
  • UnityプレイヤーをHwndHostを使ってWPFの子ウィンドウとして管理しました

あるプロセスが読み込んでいる.NET Coreランタイムを特定したい

なんか表題のことがしたいらしく、よく聞いたら任意の.NET Coreプロセスが読み込んでいるcoreclr.dllのパスが知りたいという話でした。

coreclr.dllで.NET Core仮想マシンをホストしてるんだから、プロセスが読み込んでるモジュールを列挙したらわかるでしょ。とか思って、Processの情報を眺めたら、彼はそこに常にいたので、それを探したらお悩みは解決。

どうやらC#で解決したいらしいので、とりあえずpsapiからEnumしただけでおわり。

gist.github.com

EnumProcessesでプロセスを列挙してOpenProcessにPROCESS_VM_READとPROCESS_QUERY_INFORMATIONをつけてプロセスハンドルを手に入れて、EnumProcessModulesExでモジュールハンドルを列挙して、GetModuleFileNameExでモジュールのパスを取得する。そこにcoreclr.dllがあればそいつが.NET Coreプロセスだ!

という話でした。

最近のWindows 10で絵文字を書記素クラスタ単位で数える

UWPからP/Invokeすることが許可されているAPIが列挙されているページがあります。今日も特に理由もなく眺めていたらicuuc.dllが16299から使えるようになっていたことに今更気づきました。

docs.microsoft.com

icuuc.dllはICUの一部で、ICUはInternational Components for Unicodeの略で、Unicodeの難しいいろいろを面倒見てくれるライブラリです。 今日はこれを使って絵文字を書記素クラスタ単位で数えてみたいと思います。

今回のコードはこのページを参考にしました。C++からC#に書き直しただけでほぼそのままです。

qiita.com

書記素クラスタ

詳しいことはここを読むといいと思います。

ufcpp.net

Unicodeを1文字ずつ分割するときに使うアルゴリズムです。いろいろ調べても基本的にテーブル引きしないといけないようなことが書いてました。

.NETから呼ぶ

.NETから呼ぶにはP/InvokeすればOKです。今回は、書記素クラスタに分割するAPI周りだけ定義を作りました。

gist.github.com

これを使って、こんなコードを書くと書記素クラスタに基づいて、何文字で構成された文字列なのかがわかります。 参考にしたコードでは、utext_openUTF8を使っていたけれども、.NETのStringはUTF-16なのでutext_openUCharsを使ったほうが都合がいいです。

public static int TextLengthInGrapheme(string s)
{
    var iterator = Icuuc.ubrk_open(UBreakIteratorType.UBRK_CHARACTER, Icuuc.uloc_getDefault(), null, 0, out _);
    var utext = Icuuc.utext_openUChars(IntPtr.Zero, s, s.Length, out _);
    Icuuc.ubrk_setUText(iterator, utext, out _);
    var n = 0;
    while (Icuuc.ubrk_next(iterator) != Icuuc.UBRK_DONE) n++;
    Icuuc.ubrk_close(iterator);
    Icuuc.utext_close(utext);
    return n;
}

また、文字を切り出すにはこんな感じで呼べばよさそうです。

var iterator = Icuuc.ubrk_open(UBreakIteratorType.UBRK_CHARACTER, Icuuc.uloc_getDefault(), null, 0, out _);
var utext = Icuuc.utext_openUChars(IntPtr.Zero, Text, Text.Length, out _);
Icuuc.ubrk_setUText(iterator, utext, out _);
int current = Icuuc.ubrk_current(iterator);
while (current != Icuuc.UBRK_DONE)
{
    int next = Icuuc.ubrk_next(iterator);
    if (next == Icuuc.UBRK_DONE) break;
    int size = next - current;
    Debug.WriteLine($"{current}-{next}: {Text.Substring(current, size)}");
    current = next;
}
Icuuc.ubrk_close(iterator);
Icuuc.utext_close(utext);

比べてみる

今回作ったコードと、.NETのStringInfoクラスと、String[Index]でえられる結果を並べてみました。 左から、ICU、StringInfo、String[Index]です。

f:id:tmyt:20190420205012p:plain

ICUは1文字としてカウントされてます。StringInfoはサロゲートペアを認識はしてますが、書記素分割には対応してないので11文字、String[Index]はサロゲートペアをバラバラにカウントするので19文字に見えています。 StringInfoを使っていると👩🏻‍👩🏿‍👧🏼‍👧🏾が11文字としてカウントされていたのが正しく1文字になるのでTwitterクライアントの文字カウントがより正確になります*1

おまけ

f:id:tmyt:20190420205540p:plain

Twitter…お前…!!!!

*1:Aristeaは近日対応したのがリリースされます

プロセスのことが気になって夜も眠れないのでツールを書いた

TL;DR

  • Windows 10 on ARMで動いているプロセスがi386なのか、ARM32なのか、ARM64なのか気になって夜も眠れないのでツールを書きました。
  • GitHubにあります。

夜も眠れない

Windows 10 on ARMは、i386、ARM32、ARM64のPEが実行できる、とても愉快なOSだそうです。確かに、バイナリを眺めているとARM32とかi386の実行ファイルが紛れ込んでいるのが確認できます。

でも、タスクマネージャからはそのプロセスがターゲットにしてるCPUアーキテクチャが確認できません。確認できるのは32bit or 64bitのみ。

f:id:tmyt:20190401120237p:plain

僕が知りたいのはビット数じゃなくて、CPUアーキテクチャなんです。気になって夜も眠れなくなってきました。

確認方法

IsWow64Process2 という今欲しかったものが得られるAPIがあります。呼びましょう。

BOOL IsWow64Process2(
  HANDLE hProcess,
  USHORT *pProcessMachine,
  USHORT *pNativeMachine
);

これはプロセスのハンドル*1を渡すと、pProcessMachineにプロセスが何のCPU向けのバイナリなのかを返してくれます。このとき、実行環境のネイティブバイナリの場合は IMAGE_FILE_MACHINE_UNKNOWN が入っているので、WOW64で動いているのかそうでないのかがわかります。

ソースとバイナリ

これを全プロセスに対してチェックして、リストにするような感じに仕立てておきました。

f:id:tmyt:20190401120728p:plain

ソースとバイナリはGitHubにあります。

github.com

*1:OpenProcessの引数に"PROCESS_QUERY_INFORMATION"か"PROCESS_QUERY_LIMITED_INFORMATION"が必要です

Sakura.IOをWindows 10 IoTで使うライブラリを供養した

Sakura.IOをWindows 10 IoT上で使うライブラリをBuriKaigiで話そうかと思って作ったんだけど結局使わなかった。 せっかくなのでGitHubに公開しておきました。

github.com

もう少し便利なハイレベルAPI整備したほうが使いやすいと思うんですが、とりあえずほぼArduino版のPortです。 C#/UWPなのでawaitableな関数で実装しないといけないことがあったので、せっかくなので接続完了を待つメソッドも用意しておきました。

こんな感じで使えます。

var sakuraio = new SakuraIO_I2C();
await sakuraio.OpenAsync("I2C1");
await sakuraio.WaitForConnectionAsync();
var bytes = Encoding.UTF8.GetBytes("HELOWRLD");
sakuraio.EnqueueTx(0, BitConverter.ToUInt64(bytes, 0));
sakuraio.Send();

Sakura.IO + Raspberry Pi + Windows 10 IoT Core っていうパイの小さそうな組み合わせですが使えそうならぜひどうぞ。

Lenovo C630を買いました

Microsoft MVP Global Summtでシアトルにきているので、家電量販店をのぞいたらC630が売っていたので買いました。

とりあえずこの記事もC630で書いていますが、まーまー良くも悪くもただのWindowsです。 初回起動時のパフォーマンスも使っている分にはそんなに悪いと感じない程度には普通のWindowsです。

ちなみに量販店モデルは

  • Snapdragon 850
  • RAM 8GB
  • SSD 128GB
  • Officeなし(アメリカだからね)

という構成でした。

C630はLTEモデム内蔵なので、SIMを入れれば単体で通信できます。量販店でこいつはSIM Lockedか?って確認したら、そうだと思うよ。と言われながらも、 VerizonのLTEモデルだったらSIMロックかかってないはずだしな…と思い、試しにIIJmioDocomoと、T-Mobile (アメリカの現地SIM)を入れてみたところ、ちゃんと認識しました。

最近のWindowsは、ARMで実行できるだけでなく、ARM上でWSLがちゃんと実行できるところがとても偉いと思います。 WSLとDebianをインストールして、 uname -a とすると、ちゃんと aarch64 って出ます。

以前にx64版のWSLで32bit ELFを実行できたように、同じようにQEMUを入れて設定すると*1たぶんx86 ELFも動くのでしょう。たぶん。

ほかにも。ARM版のWindowsx86バイナリを実行時にARM命令にトランスレートする機能があるので、x86のPEが実行できます。 タスクマネージャで詳細タブのプラットフォームカラムを追加したときに32bitってでるやつはたぶんx86

f:id:tmyt:20190322015637p:plain

ここに見えているChromeとOneDriveはPEヘッダを確認したところ、どちらもx86版でした。 にこれがOneDrive.exeのヘッダ部分。選択箇所が 4c 01なのでx86バイナリです。

f:id:tmyt:20190322015848p:plain

ちなみにこっちが、Windows付属のnotepad.exeのヘッダ部分。64 aaと書いてるのでARM64バイナリです。

f:id:tmyt:20190322020113p:plain

トランスレーションで実行されててもそんなに遅いと思わないので、よくもまぁあの複雑なバイナリを…という感じです。 詳しいことはこの辺*2に書いてあるそうです。

しばらくあそべそう…

BuriKaigi2019に行ってきました

早くも1か月前の話になりますが… BuriKaigi2019に行ってきました。

toyama-eng.connpass.com

ありがたいことに、なにか話していいよという時間を頂いたので、App CenterとかVSTSでCIする話をしてきました。

基本的には以前にエントリしたこれ (https://blog.tmyt.jp/entry/2018/11/26/011621) と同じですが、 せっかく.NETトラックなのでXamarin.Formsでコードを用意していった…のですが、デバイスの画面をPCに出力するアプリと APKインストーラの相性が悪いのか、いまいちちゃんと動かなかったのが残念です。

www.slideshare.net

当日は25分しかなく、.NETのドの字すらほとんどない感じで、さらにコードをほとんどお見せする余裕すらなかったので ここで供養しておきます。

NuGetパッケージの追加

App Centerの各機能を使うライブラリはさすがMicrosoftがやってるサービスなだけあって、NuGetからインストールするだけで使えます。

.NET Standardで共有される部分のライブラリプロジェクトに対してNuGetパッケージの追加

f:id:tmyt:20190224220716p:plain

AppCenterと検索するとそれらしいライブラリが出てくるのでほしいものをインストールします。 ちなみに、認証済みマークがついてないで判別できますが、2個目の "AppCenter.Analytics.Metrics" はサードパーティライブラリです。

f:id:tmyt:20190224220847p:plain

今回はCrashes, Analytics, Distributeをインストールしておきました。

コードの呼び出し

App Centerと連携するためのコードを少しだけ書きます。具体的にはApp.xaml.csのOnStart()の中に次のコードをコピペします。

AppCenter.Start("ios={Your App Secret};android={Your App Secret};uwp={Your App Secret}",
    typeof(Analytics), typeof(Crashes), typeof(Distribute));

{Your App Secret} と書かれた部分にApp Centerへ接続するGUIDっぽい文字列を指定します。これは、App Centerのプロジェクトページを開くと、ここに書いてあるやつです。

f:id:tmyt:20190224221605p:plain

iOSAndroid、UWPとそれぞれ指定できるので、プラットフォームごとにApp Centerのプロジェクトを作ってそれぞれの値を埋めます。

これでだいたいApp Centerと連携できるようになりました。

まとめ

という、これらはDocs.com のこのあたり (https://docs.microsoft.com/en-us/appcenter/sdk/getting-started/xamarin) に書いてあります。 また、App CenterでXamarinプロジェクトを作ると、OverviewのXamarin Formsタブに同じことが書いてあります。ここをコピペすればOKですね。

f:id:tmyt:20190224221840p:plain