Zenn
Open11

CsWin32 を使って F# Interactive から Win32 API を呼び出す

四ツ山伊吹四ツ山伊吹

CsWin32 の準備

以下ではコマンドライン (PowerShell) からの手順を説明する。

PowerShell #1
# 前準備
mkdir FsWin32
cd FsWin32
PowerShell #2
dotnet new classlib
rm 'Class1.cs'
dotnet add package 'Microsoft.Windows.CsWin32'
echo 'GetForegroundWindow' > 'NativeMethods.txt'
echo @'
{
  "$schema": "https://aka.ms/CsWin32.schema.json",
  "public": true
}
'@ > NativeMethods.json
dotnet build

と進めていくと、アセンブリ FsWin32.dllbin\Debug\<TFM>[1] に得られる。

脚注
  1. https://learn.microsoft.com/en-us/dotnet/standard/glossary#tfm ↩︎

四ツ山伊吹四ツ山伊吹

ポイント 1: NativeMethods.txt

Create a NativeMethods.txt file in your project directory that lists the APIs to generate code for.
Each line may consist of one of the following:

  • Exported method name (e.g. CreateFile). This may include the A or W suffix, where applicable. This may be qualified with a namespace but is only recommended in cases of ambiguity, which CsWin32 will prompt where appropriate.
  • A macro name (e.g. HRESULT_FROM_WIN32). These are generated into the same class with extern methods. Macros must be hand-authored into CsWin32, so let us know if you want to see a macro added.
  • A namespace to generate all APIs from (e.g. Windows.Win32.Storage.FileSystem would search the metadata for all APIs within that namespace and generate them).
  • Module name followed by .* to generate all methods exported from that module (e.g. Kernel32.*).
  • The name of a struct, enum, constant or interface to generate. This may be qualified with a namespace but is only recommended in cases of ambiguity, which CsWin32 will prompt where appropriate.
  • A prefix shared by many constants, followed by *, to generate all constants that share that prefix (e.g. ALG_SID_MD*).
  • A comment (i.e. any line starting with //) or white space line, which will be ignored.

(via Getting Started | CsWin32)

四ツ山伊吹四ツ山伊吹

F# Interactive からの参照

先ほど得られたアセンブリ bin\Debug\<TFM>\FsWin32.dll を F# Interactive で参照することで Win32 API を呼び出せるようになる。

参照のやり方は以下の二通りある:

コマンドラインから起動時に参照する

PowerShell
dotnet fsi -r 'bin\Debug\<TFM>\FsWin32.dll'
# 同じ
#dotnet fsi --reference:'bin\Debug\<TFM>\FsWin32.dll'

F# Interactive のディレクティブで参照する

F# Interactive
> #r @"bin\Debug\<TFM>\FsWin32.dll"

--> Referenced 'path\to\bin\Debug\<TFM>\FsWin32.dll' (file may be locked by F# Interactive process)
四ツ山伊吹四ツ山伊吹
参考:F# Interactive のディレクティブ
F# Interactive
> #help;;

  F# Interactive directives:

    #r "file.dll";;                               // Reference (dynamically load) the given DLL
    #i "package source uri";;                     // Include package source uri when searching for packages
    #I "path";;                                   // Add the given search path for referenced DLLs
    #load "file.fs" ...;;                         // Load the given file(s) as if compiled and referenced
    #time ["on"|"off"];;                          // Toggle timing on/off
    #help;;                                       // Display help
    #help "idn";;                                 // Display documentation for an identifier, e.g. #help "List.map";;
    #r "nuget:FSharp.Data, 3.1.2";;               // Load Nuget Package 'FSharp.Data' version '3.1.2'
    #r "nuget:FSharp.Data";;                      // Load Nuget Package 'FSharp.Data' with the highest version
    #clear;;                                      // Clear screen
    #quit;;                                       // Exit

  F# Interactive command line options:

      See 'dotnet fsi --help' for options
四ツ山伊吹四ツ山伊吹

参照時の注意

ディレクティブ実行された後に、F# Interactive が “file may be locked by F# Interactive process” と報告している通り。

ファイルのロックが原因でどこかで不具合が生じる可能性がある。そういうときには慌てずに F# Interactive を終了してみよう。いちど参照した DLL アセンブリをセッション中にアンロードする方法(ディレクティブ)はない。.NET API を使えばできるかも?

四ツ山伊吹四ツ山伊吹

F# Interactive からの呼び出し

はじめに NativeMethods.txt ファイルで宣言した関数は、Windows.Win32 名前空間の PInvoke クラスの静的メソッドとして呼び出しできる。

F# Interactive
> open Windows.Win32;;
> PInvoke.GetForegroundWindow;;
val it: unit -> Foundation.HWND

> PInvoke.GetForegroundWindow ();;
val it: Foundation.HWND = 0x9080a {IsNull = false;
                                   Value = System.Reflection.Pointer;}

タブ補完も問題なく効く。

F# Interactive
> PI[TAB].G[TAB] ⟶ PInvoke.GetForegroundWindow

Windows.Win32.Foundation 名前空間には、関連するデータ型が自動で作られる。

F# Interactive
> typedefof<PInvoke>.Assembly.GetTypes()
- |> Seq.filter (fun x -> x.Namespace = "Windows.Win32.Foundation")
- |> Seq.map (fun x -> string x);;
val it: string seq =
  seq
    ["Windows.Win32.Foundation.BOOL"; "Windows.Win32.Foundation.HANDLE";
     "Windows.Win32.Foundation.HWND"]
四ツ山伊吹四ツ山伊吹

CsWin32 の疑問点

  • ソースジェネレーターによって生成されたファイルの実体はどういう規則に従ってどこへ保存されているのだろうか?
  • 関数に対応する名前空間はどこで分かるのか?(“A namespace to generate all APIs from (e.g. Windows.Win32.Storage.FileSystem would search the metadata for all APIs within that namespace and generate them)” の部分)
ログインするとコメントできます