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 を使用します。
> #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# では参照を追加しても動きません。
> #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
がないため結果が取得できません。
> 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# と同じような扱い方をします。
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
をリフレクションで呼んで変換します。
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# に移植します。
#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
の続きで使ってみます。
> let file = result |> await StorageFile;;
val file : obj = Windows.Storage.StorageFile
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
について説明されています。