詳細

Win32モジュール

このモジュールにはINIファイル用のWin32 APIをP/Invokeするための定義があります。
今回はINIファイルを読み取る機能しかサポートしないつもりなので、以下の関数しか定義していません。

Wなのは特に強い動機があるわけではないです。
ただし読み取り対象のINIファイルをBOMありUTF-16で保存する必要がある点に注意です。

Utilモジュール

このモジュールはWin32モジュールのラッパー的なものです。
例えばGetPrivateProfileSectionNamesWは全てのセクション名をNULL値('\0')区切りで返すので、それをSystem.Stringの配列として取得できるようにしてます。

IniSection型、IniFile型

INIファイルの中身を表します。
これらの型はType Provider経由で公開される型のベースとなるものです。
つまり、これらの型に定義されたメンバ+Type Providerで動的に用意されるメンバが使えるようになる予定です。

IniFile型

INIファイルに含まれる全てのセクション名を返すSectionsプロパティがあります。
また、特定のセクションをIniSection型として取得できるGetSectionメソッドもあります。

IniSection型

セクションに含まれる全てのキー名を返すKeysプロパティがあります。
また、キーの値を文字列として取得するGetValueメソッドもあります。

IniFileTypeProvider型

INIファイル用のType Providerを実装する型です。

Type Providerの実装に必要な属性:TypeProviderAttribute, TypeProviderAssemblyAttribute

Type Providerを実装する場合、型にはTypeProviderAttributeを指定する必要があります。

[<TypeProvider>]
type IniFileTypeProvider(config : TypeProviderConfig) as this =

また、アセンブリ自体にもTypeProviderAssemblyAttributeを指定しなければいけません。

[<assembly:TypeProviderAssembly>]
do ()

そのほかに、Type Providerを実装する型はITypeProviderインターフェイスを実装しないといけないのですが、これはFsharpxのTypeProviderForNamespaces型で実装されているので楽できます。

type IniFileTypeProvider(config : TypeProviderConfig) as this =
    inherit TypeProviderForNamespaces()
Type Providerとして公開する型の定義

ProvidedTypeDefinitionを作った後、TypeProviderForNamespacesのAddNamespaceに渡してあげるだけで型を公開できます。
なんと簡単なことでしょうか。

    let asm = Assembly.GetExecutingAssembly()
    let ns = "Personal.FSharp.TypeProviders"
    let iniTy = ProvidedTypeDefinition(asm, ns, "Ini", Some(typeof<obj>))
(中略)
    do this.AddNamespace(ns, [ iniTy ])

こうするだけで、このアセンブリを参照するコードからは「Personal.FSharp.TypeProviders.Ini」という型が使えるようになります。

static引数の定義

static引数はProvidedTypeDefinitionのDefineStaticParametersメソッドを呼ぶことで追加できます。
このメソッドの1番目の引数には、ProvidedStaticParameterのリストを指定できます。
今回は対象となるINIファイルのパスが1つ指定できればよいので、ProvidedStaticParameterのインスタンス1つを渡します。

    let filename = ProvidedStaticParameter("filename", typeof<string>)
    do iniTy.DefineStaticParameters([filename],

2番目の引数には、static引数を受け取った際に何をするかという、Type Providerのまさに本体となるコードを渡します。

    do iniTy.DefineStaticParameters([filename], applyFunc)
applyFunc
    let applyFunc = fun (tyName:string) (parameters:obj[]) ->

このメソッドの1番目には、Type Providerとして呼び出された型の名前が渡ってきます。
今回の場合、「Ini, filename="xxxxx.ini"」みたいな文字列が受け取れるんですが、たぶんこの値を使って何か動作を変えるというケースは稀なんじゃないかと思います。

2番目の引数には、static引数で指定された値がobjectの配列として渡ってきます。
なので実装的には適切な型へとキャストして使うことになります。

        match parameters with
        | [| :? string as filename |] ->

今回は要素が1つで、しかも文字列型の場合にだけ機能するように実装します。

static引数付きで呼ばれた後の動作を決定する

先に説明した通り、IniFile型をベースにして、そこにさらに各セクション名、さらに各キー名を持ったプロパティを追加したいという目論見です。

let ini = new Personal.FSharp.TypeProviders.Ini<"Test.ini">()
printfn "%s" ini.MySection1.MyKey1

みたいなコードで使えるようにしたいという感じです。
そこでまずはIniFile型をベースにしたProvidedTypeDefinitionを用意します。

            let ty = ProvidedTypeDefinition(asm, ns, tyName, Some(typeof<IniFile>))

さらに、この型にコンストラクタを追加します。

            let resolvedFilename = Path.GetFullPath(Path.Combine(config.ResolutionFolder, filename))
            let ctor0 = ProvidedConstructor([],
                            InvokeCode = fun [] -> <@@ IniFile(resolvedFilename) @@>)
            ty.AddMember ctor0

ProvidedConstructorという名前と、1番目の引数[]からもわかる通り、これは引数無しのコンストラクタ定義です。
また、呼び出された時にどうするべきかというコードがコードクォートとしてInvokeCodeプロパティに指定しています。
コードクォート内ではIniFile型のコンストラクタを呼び出すだけですが、INIファイルの名前はTypeProviderConfigのResolutionFolderプロパティとの組み合わせになっています。
こうすることで、相対パスでファイルを指定してもうまいことファイルが見つかるようになるような気がします。

このように、公開する型にメンバを追加するにはAddMemberを呼びます。
AddMemberにはProvidedConstructorやProvidedProperty、ProvidedMethodなどのインスタンスを指定できます。
これらのProvidedXXX型は先のFsharpxコードで定義されています。
至れり尽くせりです。
さらに言えば、AddXmlDocメソッドを呼び出して各メンバにドキュメントコメントを用意するべきなんですが、ここでは割愛します。

そしていったんIniFileのインスタンスを作ってから、各セクションと各キーを走査して、プロパティを追加していきます。

            // それぞれのセクションと同じ名前のプロパティを追加
            iniFile.Sections
            |> Seq.iter (fun section ->
                let sectionTy = ProvidedTypeDefinition(section, Some(typeof<IniSection>))

                // それぞれのキーと同じ名前のプロパティを追加
                iniFile.GetSection(section).Keys
                |> Seq.iter (fun key ->
                    let keyProp = ProvidedProperty(key, typeof<string>,
                                    GetterCode = fun args -> <@@ (%%args.[0] : IniSection).GetValue(key, null) @@>)
                    sectionTy.AddMember keyProp)

                let prop = ProvidedProperty(section, sectionTy,
                            GetterCode = fun args -> <@@ (%%args.[0] : IniFile).GetSection(section) @@>)
                ty.AddMember prop

                ty.AddMember sectionTy)

興味深いところといえば、ProvidedPropertyのGetterCodeに指定しているコードクォートでしょうか。

                                    GetterCode = fun args -> <@@ (%%args.[0] : IniSection).GetValue(key, null) @@>)
(中略)
                            GetterCode = fun args -> <@@ (%%args.[0] : IniFile).GetSection(section) @@>)

このargs引数の1番目にはthisが渡ってきているので、それを適切な型にキャストしてからメンバーメソッドを呼び出すようにしています。
以上で完成です。