Type Provider for INI file (draft)
This entry is for F# Advent Calendar 2012, 9th.
Previous one is Mono for Android with F#, written by @smallgeek.
It's very interesting one, and I really want to make some exciting Game(s).
# But I know that I don't have enough skills to do that yet X(
Here, I'm trying to implement a Type Provider for INI file with using Fsharpx, ProvidedTypes.fs.
BTW, I couldn't use some types defined in ProvidedTypes.fs only when installing the package via Nuget package manager by running "Install-Package FSharpx.TypeProviders" command.
However, these types are virtually necessary to implement custom Type Providers, I think.
So, it's very nice that they're available via Nuget.
First, the following is an entire implementation code:
続きを読むTips to Debug Type Provider
Type Providerをデバッグする場合は以下のようにすると色々はかどるんじゃないかと思います。
- Type Providerを作成中のプロジェクトのプロパティで[デバッグ]の[開始動作]を[外部プログラムの開始]にして「devenv.exe」へのパスを指定する
- [開始オプション]では別途作成したテスト用のプロジェクトを含むソリューションのファイルパスを指定する
あと、テスト用のプロジェクトではDebugとReleaseのビルドに対応できるように、fsprojファイルを編集して
..\IniTypeProvider\bin\$(ConfigurationName)\IniTypeProvider.dll
動作確認
1.別のVisual Studioを起動して、[F# アプリケーション]のプロジェクトを新規作成します。
2.作成したアセンブリを参照追加します。
2.1.[型プロバイダーのセキュリティ]ダイアログが表示されるので、[有効化]を選択します
3.テスト用のINIファイルを用意します。
3.1.INIファイルはBOMありUTF-16のエンコーディングで保存するようにします。
[MySection1] MyKey1=MyValue1 MyKey2 [MySection2] MyKey1=MyValue1 MyKey2=MyValue2 MyKey3=MyValue3 [MySection3]
4.テスト用のコードを用意します。
let ini = new Personal.FSharp.TypeProviders.Ini<"Test.ini">()
そうするとIntelliSenseが機能して、いい感じになるんじゃないかと思います。
[<EntryPoint>] let main argv = let ini = new Personal.FSharp.TypeProviders.Ini<"Test.ini">() printfn "%s" ini.MySection1.MyKey1 printfn "%s" ini.MySection1.MyKey2 0 // 整数の終了コードを返します
先のINIファイルを使って上のコードを実行すると、結果は以下のようになります。
MyValue1 続行するには何かキーを押してください . . .
詳細
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が渡ってきているので、それを適切な型にキャストしてからメンバーメソッドを呼び出すようにしています。
以上で完成です。
Type Provider for INI file (draft)
namespace Personal.FSharp.TypeProviders open System open System.IO open System.Reflection open System.Runtime.InteropServices open System.Text open Microsoft.FSharp.Core.CompilerServices open Samples.FSharp.ProvidedTypes module Win32 = [<DllImport("kernel32", CharSet = CharSet.Unicode)>] extern int GetPrivateProfileSectionNamesW( byte[] lpszReturnBuffer, int nSize, string lpFileName) [<DllImport("kernel32", CharSet = CharSet.Unicode)>] extern int GetPrivateProfileSectionW( string lpAppName, byte[] lpReturnedString, int nSize, string lpFileName) [<DllImport("kernel32", CharSet = CharSet.Unicode)>] extern int GetPrivateProfileStringW( string lpAppName, string lpKeyName, string lpDefault, StringBuilder lpReturnedString, int nSize, string lpFileName) module Util = let private SplitNullTerminatedBuffer(buf : byte[], count : int) = // wchar用に文字数の倍の数分だけバッファを読む let namesBuf = Encoding.Unicode.GetString(buf, 0, count*2) namesBuf.Split([| (char)0 |], StringSplitOptions.RemoveEmptyEntries) /// <summary>INIファイル内にあるすべてのセクション名を取得します。</summary> /// <param name="fileName">INIファイルの名前。</param> let GetAllSectionNames(fileName) = let buf = Array.zeroCreate 10240 let numBuffer = Win32.GetPrivateProfileSectionNamesW(buf, buf.Length, fileName) SplitNullTerminatedBuffer(buf, numBuffer) /// <summary>INIファイル内の特定セクションにあるすべてのキーを取得します。</summary> /// <param name="fileName">INIファイルの名前。</param> /// <param name="sectionName">INIファイル内にあるセクション名。</param> let GetAllKeysInSection(fileName, sectionName) = let buf = Array.zeroCreate 10240 let numBuffer = Win32.GetPrivateProfileSectionW(sectionName, buf, buf.Length, fileName) let values = SplitNullTerminatedBuffer(buf, numBuffer) seq { for i in 0 .. values.Length - 1 do let kvp = values.[i].Split([|'='|], 2) yield kvp.[0] } /// <summary>INIファイル内の特定セクションにあるキーの値を取得します。</summary> /// <param name="fileName">INIファイルの名前。</param> /// <param name="sectionName">INIファイル内にあるセクション名。</param> /// <param name="keyName">取得したい値を持つキーの名前。</param> /// <param name="defaultValue">デフォルト値。</param> let GetSectionValue(fileName, sectionName, keyName, defaultValue) = let buf = new StringBuilder(10240) Win32.GetPrivateProfileStringW(sectionName, keyName, defaultValue, buf, buf.Capacity, fileName) |> ignore buf.ToString() /// <summary>INIファイル内にあるセクションを表します。</summary> /// <param name="fileName">INIファイルの名前。</param> /// <param name="sectionName">INIファイル内にあるセクション名。</param> type IniSection(fileName, sectionName) = let keys = seq { for key in Util.GetAllKeysInSection(fileName, sectionName) do yield key } /// 現在のセクションの名前。 member __.Name = sectionName /// 現在のセクション内にあるすべてのキー。 member __.Keys = keys |> Seq.toArray /// <summary> /// 現在のセクション内にある特定のキーの値を取得します。 /// <para>キーが見つからない場合、<paramref name="defaultValue">の値が返されます。</para> /// </summary> /// <param name="key">取得したい値を持つキーの名前。</param> /// <param name="defaultValue">デフォルト値。</param> member __.GetValue(key, defaultValue) = Util.GetSectionValue(fileName, sectionName, key, defaultValue) /// <summary>INIファイルを表します。</summary> /// <param name="fileName">INIファイルの名前。</param> type IniFile(fileName) = let sections = seq { for name in Util.GetAllSectionNames(fileName) do yield name } |> Seq.cache /// INIファイルの名前。 member __.FileName = fileName /// INIファイル内にあるすべてのセクション名。 member __.Sections = sections |> Seq.toArray /// <summary>INIファイル内にある特定のセクションを取得します。</summary> /// <param name="name">INIファイル内にあるセクション名。</param> member __.GetSection(name) = IniSection(fileName, name) /// <summary>INIファイル用のType Providerを実装する型。</summary> /// <param name="config"><see ref="Microsoft.FSharp.Core.CompilerServices.TypeProviderConfig"/>による構成をサポートします。</param> [<TypeProvider>] type IniFileTypeProvider(config : TypeProviderConfig) as this = inherit TypeProviderForNamespaces() let asm = Assembly.GetExecutingAssembly() let ns = "Personal.FSharp.TypeProviders" // INIファイル用Type Providerの起点となる型の定義。 // このインスタンスに様々なメンバや型定義を追加していきます。 let iniTy = ProvidedTypeDefinition(asm, ns, "Ini", Some(typeof<obj>)) // Type Provider使用時にstatic引数で指定された値。 // 今回はINIファイルの名前を指定できるようにするので、 // string型のProvidedStaticParameterを1つ用意します。 let filename = ProvidedStaticParameter("filename", typeof<string>) // static引数の実体。 let applyFunc = fun (tyName:string) (parameters:obj[]) -> match parameters with | [| :? string as filename |] -> let ty = ProvidedTypeDefinition(asm, ns, tyName, Some(typeof<IniFile>)) let resolvedFilename = Path.GetFullPath(Path.Combine(config.ResolutionFolder, filename)) let iniFile = new IniFile(resolvedFilename) // それぞれのセクションと同じ名前のプロパティを追加 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) let ctor0 = ProvidedConstructor([], InvokeCode = fun [] -> <@@ IniFile(resolvedFilename) @@>) ty.AddMember ctor0 ty | _ -> failwith "Invalid parameter" do iniTy.DefineStaticParameters([filename], applyFunc) do this.AddNamespace(ns, [ iniTy ]) [<assembly:TypeProviderAssembly>] do ()