Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

WinRTの非同期メソッドをリフレクションで扱う

WinRT は通常の .NET アセンブリとは異なるため言語側でもサポートが必要です。特に WinRT で多用される非同期処理が問題になります。await に相当するコードを PowerShell から F# に移植して対応します。

WinRT が COM ベースで動いていることが垣間見えて面白いです。

今回の手法でコードを書いてみた記事です。

概要

記事執筆時点で F# には WinRT のサポートがないため、リフレクション経由で扱う必要があります。PowerShell ではリフレクションは裏側で処理されるため、F# ほど煩雑にはなりません。

非同期メソッドは __ComObject を返します。C# では await で処理できますが、F# や PowerShell では WindowsRuntimeSystemExtensions.AsTask()Task に変換する必要があります。

【追記】コメント欄の情報より、開発中の C#/WinRT によって改善する見込みとのことです。

StorageFile.GetFileFromPathAsync()C:\test.txt を開く例です。ファイル名は絶対パスで指定する必要があります。

C# では WinRT がサポートされているため、参照を追加すれば動きます。動作中の型を示すため C# Interactive を使用します。

C#
> #r "System.Runtime.WindowsRuntime"
> #r "C:\Program Files (x86)\Windows Kits\10\UnionMetadata\10.0.17763.0\Facade\windows.winmd"
> #r "C:\Program Files (x86)\Windows Kits\10\References\10.0.17763.0\Windows.Foundation.FoundationContract\3.0.0.0\Windows.Foundation.FoundationContract.winmd"
> #r "C:\Program Files (x86)\Windows Kits\10\References\10.0.17763.0\Windows.Foundation.UniversalApiContract\7.0.0.0\Windows.Foundation.UniversalApiContract.winmd"
> var result = Windows.Storage.StorageFile.GetFileFromPathAsync(@"C:\test.txt");
> result
[System.__ComObject]
> var file = await result;
> file
[Windows.Storage.StorageFile]

F# では参照を追加しても動きません。

F#
> #r "System.Runtime.WindowsRuntime";;
(略)
> #r @"C:\Program Files (x86)\Windows Kits\10\UnionMetadata\10.0.17763.0\Facade\windows.winmd";;
(略)
> #r @"C:\Program Files (x86)\Windows Kits\10\References\10.0.17763.0\Windows.Foundation.FoundationContract\3.0.0.0\Windows.Foundation.FoundationContract.winmd";;
(略)
> #r @"C:\Program Files (x86)\Windows Kits\10\References\10.0.17763.0\Windows.Foundation.UniversalApiContract\7.0.0.0\Windows.Foundation.UniversalApiContract.winmd";;
(略)
> let result = Windows.Storage.StorageFile.GetFileFromPathAsync(@"C:\test.txt");;

error FS0193: ファイルまたはアセンブリ 'file:///C:\Program Files (x86)\Windows Kits\10\References\10.0.17763.0\Windows.F
oundation.FoundationContract\3.0.0.0\Windows.Foundation.FoundationContract.winmd'、またはその依存関係の 1 つが読み込めま
せんでした。操作はサポートされません。 (HRESULT からの例外:0x80131515)

ランタイムは WinRT を認識するため、参照は追加せずにリフレクションで扱います。しかし await がないため結果が取得できません。

F#
> let StorageFile = System.Type.GetType("Windows.Storage.StorageFile,Windows.Foundation.UniversalApiContract,ContentType=WindowsRuntime");;
val StorageFile : System.Type = Windows.Storage.StorageFile

> let result = StorageFile.GetMethod("GetFileFromPathAsync").Invoke(null, [|@"C:\test.txt"|]);;
val result : obj = System.__ComObject

PowerShell はリフレクションが表には出て来ませんが、F# と同じような扱い方をします。

PowerShell
PS> [Windows.Storage.StorageFile,Windows.Foundation.UniversalApiContract,ContentType=WindowsRuntime]

IsPublic IsSerial Name                             BaseType
-------- -------- ----                             --------
True     False    StorageFile                      System.Runtime.InteropServices.WindowsRuntime.RuntimeClass

PS> $result = [Windows.Storage.StorageFile]::GetFileFromPathAsync("C:\test.txt")
PS> $result
System.__ComObject

※ この __ComObject は従来の COM とは異なり、IDispatch ではなく IInspectable ベースです。

Task に変換

以下の記事に PowerShell で Task に変換する方法があります。WindowsRuntimeSystemExtensions.AsTask をリフレクションで呼んで変換します。

PowerShell
Add-Type -AssemblyName System.Runtime.WindowsRuntime
$asTaskGeneric = ([System.WindowsRuntimeSystemExtensions].GetMethods() | ? { $_.Name -eq 'AsTask' -and $_.GetParameters().Count -eq 1 -and $_.GetParameters()[0].ParameterType.Name -eq 'IAsyncOperation`1' })[0]
Function Await($WinRtTask, $ResultType) {
 $asTask = $asTaskGeneric.MakeGenericMethod($ResultType)
 $netTask = $asTask.Invoke($null, @($WinRtTask))
 $netTask.Wait(-1) | Out-Null
 $netTask.Result
}

これを F# に移植します。

F#
#r "System.Runtime.WindowsRuntime"
open System
let AsTaskGeneric = query {
    for m in typeof<WindowsRuntimeSystemExtensions>.GetMethods() do
    where (m.Name = "AsTask")
    let ps = m.GetParameters()
    where (ps.Length = 1 && ps.[0].ParameterType.Name = "IAsyncOperation`1")
    select m
    exactlyOne }
let await resultType winRtTask =
    let asTask = AsTaskGeneric.MakeGenericMethod [|resultType|]
    let task = asTask.Invoke(null, [|winRtTask|])
    task.GetType().GetProperty("Result").GetValue(task)
  • パイプライン演算子との相性を考えて await の引数は逆にしました。
  • Wait(-1) がなくても Result で結果を待つため、F# では省略しました。

結果

先ほどの GetFileFromPathAsync の続きで使ってみます。

F#
> let file = result |> await StorageFile;;
val file : obj = Windows.Storage.StorageFile
PowerShell
PS> $file = Await $result ([Windows.Storage.StorageFile])
PS> $file

ContentType      : text/plain
FileType         : .txt
IsAvailable      : True
Attributes       : Archive
DateCreated      : 2020/06/23 22:20:30 +09:00
Name             : test.txt
Path             : C:\test.txt
DisplayName      : test
DisplayType      : テキスト ドキュメント
FolderRelativeId : 9040E69AD829A77C\test.txt
Properties       : Windows.Storage.FileProperties.StorageItemContentProperties
Provider         : Windows.Storage.StorageProvider

無事に結果が取得できました。

AsTask

AsTask には大量のオーバーロードがあります。

このうち引数が1つだけのものは以下の4つの型を受け取ります。

  • IAsyncOperation<TResult>
  • IAsyncOperationWithProgress<TResult, TProgress>
  • IAsyncAction
  • IAsyncActionWithProgress<TProgress>

非同期メソッドはこれらのどれかを返します。今回は IAsyncOperation<TResult> だけを実装しましたが、すべてサポートする必要があります。

参考

PowerShell での WinRT について参考にしました。

C# での参照の追加について参考にしました。

F# でリフレクションでメンバーを動的にルックアップする方法が紹介されています。

AsTask について説明されています。

7shi
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away