2016年4月19日火曜日

Windowsの各種Timerの精度について

WindowsはいくつかのTimerを提供しています。

があり、それぞれに特徴があります。
などはよく知られていると思います。しかし実際どうなっているのか気になって調べたところOn WinAPI timers and their resolutionという比較記事を見つけました。しかしOSバージョンが明示されていないなど疑問は解消しなかったため、自分で比較してみることにしました。

2016年2月2日火曜日

C++からWindows APIを呼び易くする

Windows APIの多くはC言語を前提としています。次のように戻り値がHRESULTなどのエラーコードとなり、真の戻り値は関数の最後の引数にポインターとして返される構造をしているものが多々あります。

HRESULT Direct3DCreate9Ex( UINT SDKVersion, IDirect3D9EX **ppD3D );
これをC++言語から扱いやすくしたいと思います。
一般的には次のようなcheck()関数で異常値については例外を投げることになるでしょう。
void check(HRESULT hr){ if(FAILED(hr)) throw hr; }
本題は真の戻り値です。
API関数には任意の引数があるため最後の引数を扱うのは困難です。幸いC++言語には可変長テンプレート引数があり、それをうまく扱うstd::tupleクラスとstd::tuple_elementクラスがあります。次のようなlastクラスを定義できます。
template<class... Args> struct last : std::tuple_element<sizeof...(Args)-1, std::tuple<Args...>> {};
これは例えば int, double, std::string のような型リストがあった場合にまずはstd::tuple<int, double, std::string>型を作り、これに対して最後の型を取り出します。その結果、last<int, double, std::string>::typeはstd::stringに展開されます。
ここまでくれば関数については簡単に書けるかもしれません。テンプレートと特殊化を使います。
template<class Func> struct last_argument; template<class Ret, class... Args> struct last_argument<Ret(Args...)> : last<Args...> {};
これで例えば last_argument<Direct3DCreate9Ex>::typeはlast<UINT, IDirect3D9EX**>::typeに展開され最終的にIDirect3D9EX**が得られます。
ところが、Visual C++には様々な呼び出し規約があるため、このままではデフォルトの呼び出し規約の関数にしか対応できていません。
幸いVisual C++自身もこの問題に直面していてこれを解決するマクロを使用しているため、ここではそれを流用します。ついでにメンバー関数にも対応しておきます。
template<class Func> struct last_argument; #define LAST_ARGUMENT(CALL_OPT) \ template<class Ret, class... Args> struct last_argument<Ret CALL_OPT(Args...)> : last<Args...> {}; \ template<class Ret, class... Args> struct last_argument<Ret (CALL_OPT*)(Args...)> : last<Args...> {}; _NON_MEMBER_CALL(LAST_ARGUMENT) #undef LAST_ARGUMENT #define LAST_ARGUMENT(CALL_OPT, CV_OPT, REF_OPT) \ template<class Class, class Ret, class... Args> struct last_argument<Ret(CALL_OPT Class::*)(Args...) CV_OPT REF_OPT> : last<Args...> {}; _MEMBER_CALL_CV_REF(LAST_ARGUMENT) #undef LAST_ARGUMENT
さてこれらを使った関数を用意します。std::invoke()を使うと関数呼び出しが簡単に表現できます。
template<class Func, class... Args, class Result = std::remove_pointer_t<last_argument<Func>::type>> auto get(Func func, Args&&... args) { Result result; check(std::invoke(func, std::forward(args)..., &result)); return result; } #define GET(OBJECT, METHOD, ...) get(&std::remove_reference_t<decltype(OBJECT)>::METHOD, OBJECT, __VA_ARGS__)
以上を使うと
HRESULT hr; IDirect3D9EX* d3d; hr = Direct3DCreate9Ex(D3D_SDK_VERSION, &d3d); if (FAILED(hr)) throw hr; IDirect3DDevice9Ex* d3device; hr = d3d->CreateDeviceEx(引数, いろ, いろ, &d3device); if (FAILED(hr)) throw hr;
と書いていたものが
auto d3d = get(Direct3DCreate9Ex, D3D_SDK_VERSION); auto d3device = GET(d3d, CreateDeviceEx, 引数, いろ, いろ);
と書けるようになりました。
改めてまとめると
void check(HRESULT hr){ if(FAILED(hr)) throw hr; } namespace details { template<class... Args> struct last : std::tuple_element<sizeof...(Args)-1, std::tuple<Args...>> {}; template<class Func> struct last_argument; #define LAST_ARGUMENT(CALL_OPT) \ template<class Ret, class... Args> struct last_argument<Ret CALL_OPT(Args...)> : last<Args...> {}; \ template<class Ret, class... Args> struct last_argument<Ret (CALL_OPT*)(Args...)> : last<Args...> {}; _NON_MEMBER_CALL(LAST_ARGUMENT) #undef LAST_ARGUMENT #define LAST_ARGUMENT(CALL_OPT, CV_OPT, REF_OPT) \ template<class Class, class Ret, class... Args> struct last_argument<Ret(CALL_OPT Class::*)(Args...) CV_OPT REF_OPT> : last<Args...> {}; _MEMBER_CALL_CV_REF(LAST_ARGUMENT) #undef LAST_ARGUMENT } template<class Func, class... Args, class Result = std::remove_pointer_t<details::last_argument<Func>::type>> auto get(Func func, Args&&... args) { Result result; check(std::invoke(func, std::forward(args)..., &result)); return result; } #define GET(OBJECT, METHOD, ...) get(&std::remove_reference_t<decltype(OBJECT)>::METHOD, OBJECT, __VA_ARGS__)
このコードはVisual Studio 2015 Update1のC++コンパイラーにて正常動作するとともにIDEのIntelliSenseでも正しく解釈されることを確認しています。 ただし、x64についてはIntelliSenseの問題により解釈できないようです。原因はx64では__cdeclと__stdcallの呼び出し規約が同一視されるにもかかわらず、IntelliSenseは異なる関数として扱うためテンプレート展開に失敗するためです。

2014年12月24日水曜日

F#における非同期Socket

.NET FrameworkにはVersion 1.0からSocketクラスがあり、APM; Asynchronous Programming Modelと呼ばれるBegin / End系メソッドが用意されています。しかし実際にはIOの度にIAsyncResultオブジェクトを作成する必要があり、ハイパフォーマンスなアプリケーションは実現しづらいものでした。 そのため、Version 2.0 SP1にてSocketAsyncEventArgsクラス及びAsync系のメソッドが新規に追加されました。こちらは内部状態を持つSocketAsyncEventArgsクラスを再利用することで効率の良い非同期処理が行えるものとなっています。なお、Version 4.5で導入された非同期処理とメソッド名の命名規則が一致していますが全くの別物となっています。
これをF#で扱えないものかと検索したところF#-friendly SocketAsyncEventArgsを見つけました。ただし、残念なことにSocketAsyncEventArgsクラスの設計思想を意識されておらず、毎回SocketAsyncEventArgsオブジェクトを再作成するだけの単なるwrapperでしかありませんでした。さらに言えばF#には非同期ワークフローもありますからこちらも利用したいところです。 仕方がないので自作してみました。 書いただけでまだ使っていないので動くかわかりません。
蛇足ですが、Socketクラスは内部でWinsockを使っていますが、このWinsockの機能の一つにaccept()で接続を受け付けると同時にrecv()を行うことができます。また対称にconnect()と同時にsend()もできます。こうすることでHTTPなど一般的なプロトコルでリクエストの送受信ができ、システムコール回数を減らし、システムの応答性能が向上します。Socketクラスはこの機能に対応しているため、今回の拡張メソッドにも含めています。

2014年9月10日水曜日

WPF ListView (GridView) のソート

WPFにはListViewのGridViewモードとDataGridの2つのコントロールで表形式の表示が行えます。DataGridの方はソート機能が組み込まれていますが、ListViewの方は自前でソートコードを記述する必要があります。MSDNにも方法 : ヘッダーがクリックされたときに GridView 列を並べ替えるという記事が用意されていたりしますがいまいちパッとしません。 そこで簡単に扱えるようにライブラリ化しました。 使い方は

<ListView ItemsSource="{Binding SelectedValue.Files, ElementName=tree}" xmlns:v="clr-namespace:Sayuri.Windows;assembly=GridViewSortLibrary" v:GridViewSort.IsEnabled="True"> <ListView.ItemContainerStyle> <Style TargetType="ListViewItem"> <Setter Property="HorizontalContentAlignment" Value="Stretch" /> </Style> </ListView.ItemContainerStyle> <ListView.View> <GridView> <GridViewColumn Header="名前" DisplayMemberBinding="{Binding Name}" /> <GridViewColumn Header="サイズ" v:GridViewSort.MemberPath="Length"> <GridViewColumn.CellTemplate> <DataTemplate> <TextBlock TextAlignment="Right" Text="{Binding Length, StringFormat=N0}" /> </DataTemplate> </GridViewColumn.CellTemplate> </GridViewColumn> </GridView> </ListView.View> </ListView>
こんな感じです。要点は
  • xmlnsでアセンブリ・名前空間を指定します
  • <ListView>に添付プロパティGridViewSort.IsEnabled="True"を指定します
  • <GridViewColumn>にDisplayMemberBindingの指定があればそのプロパティでソートが行われます
  • <GridViewColumn>にDisplayMemberBindingを指定できない場合はGridViewSort.MemberPathにプロパティ名をしてします
ソースコードはGistに貼り付けておきました。
作成にあたって次の2つの記事を参考にしました。

2014年8月16日土曜日

F# のアセンブリ表現

F#は言語としてはとても素晴らしい設計をしていますが、コンパイルされたアセンブリは結構残念だったりします。F# + Entity Framewrok で ASP.NET WebAPI サーバ立てたら、返ってくる JSON がおかしいという記事を見かけたのでスコープについてまとめておこうと思います。 まずF#言語でアクセス制御するためにはpublic internal privateの3つのキーワードが用意されています。次にアセンブリでのアクセス制御についてはCorHdr.hに定義されています。関連部分を引用すると

// TypeDef/ExportedType attr bits, used by DefineTypeDef. typedef enum CorTypeAttr { // Use this mask to retrieve the type visibility information. tdVisibilityMask = 0x00000007, tdNotPublic = 0x00000000, // Class is not public scope. tdPublic = 0x00000001, // Class is public scope. tdNestedPublic = 0x00000002, // Class is nested with public visibility. tdNestedPrivate = 0x00000003, // Class is nested with private visibility. tdNestedFamily = 0x00000004, // Class is nested with family visibility. tdNestedAssembly = 0x00000005, // Class is nested with assembly visibility. tdNestedFamANDAssem = 0x00000006, // Class is nested with family and assembly visibility. tdNestedFamORAssem = 0x00000007, // Class is nested with family or assembly visibility. ... } CorTypeAttr; // MethodDef attr bits, Used by DefineMethod. typedef enum CorMethodAttr { // member access mask - Use this mask to retrieve accessibility information. mdMemberAccessMask = 0x0007, mdPrivateScope = 0x0000, // Member not referenceable. mdPrivate = 0x0001, // Accessible only by the parent type. mdFamANDAssem = 0x0002, // Accessible by sub-types only in this Assembly. mdAssem = 0x0003, // Accessibly by anyone in the Assembly. mdFamily = 0x0004, // Accessible only by type and sub-types. mdFamORAssem = 0x0005, // Accessibly by sub-types anywhere, plus anyone in assembly. mdPublic = 0x0006, // Accessibly by anyone who has visibility to this scope. // end member access mask ... } CorMethodAttr;
という感じです。 先にC#言語のアクセス制御の説明をしておくとわかりやすいでしょうか。C#言語ではpublic private protected internal protected internalの5種類です。これがアセンブリとどのような対応をしているかというと非常にわかりやすく
C#クラスアセンブリ表現
publictdPublic
internaltdNotPublic
C#メンバーアセンブリ表現
publicmdPublic
privatemdPrivate
protectedmdFamily
internalmdAssem
protected internalmdFamORAssem
となります。 さて本題のF#言語ですが、C#より種類が少ないはずなのに予想を超える複雑さをしています。まずクラスですが、
F#クラスアセンブリ表現C#相当クラス
publictdPublicpublic
privatetdNotPublicinternal
internaltdNotPublicinternal
と順当です。次にメンバーですが
F#メンバーアセンブリ表現C#相当メンバー
publicmdPublicpublic
privatemdAsseminternal
internalmdAsseminternal
…はい、F#上ではクラス外からアクセスできなくなるprivateですがC#のinternalに相当しアクセス可能となります。まだ簡単に見えますか? 恐ろしいのはここからです。F#にもC#にも自動実装プロパティがあります。プロパティの値を保持するためにコンパイラが自動的にフィールド(backing field)を用意しプロパティアクセッサを実装する機能です。当然、コンパイラによる自動的なフィールドである以上、プログラムからアクセスできるべきではありません。実際、C#ではmdPrivate、privateフィールドと同等です。さてF#はそうではありません。
F#メンバーアセンブリ表現C#相当メンバー
publicmdPublicpublic
privatemdAsseminternal
internalmdAsseminternal
backing fieldmdAsseminternal
…はい、privateメンバーと同様にC#のinternal fieldに相当します。まだ簡単に見えますか? 実はこの表はまだ不完全です。F#ではクラス自身のスコープがメンバーのスコープに影響を与えます。
F#クラスF#メンバーアセンブリ表現C#相当メンバー
publicpublicmdPublicpublic
privatemdAsseminternal
internalmdAsseminternal
backing fieldmdAsseminternal
privatepublicmdAsseminternal
privatemdAsseminternal
internalmdAsseminternal
backing fieldmdAsseminternal
internalpublicmdAsseminternal
privatemdAsseminternal
internalmdAsseminternal
backing fieldmdAsseminternal
…要するにpublicクラスのpublicメンバーだけがC#のpublic相当であり、それ以外はなんであれ全てC#のinternal相当です。ちなみにmutableでないフィールドにfdInitOnly(C#におけるreadonly)が付けられていないため、アセンブリ内からであれば書き換え可能という問題もあります。 これを踏まえて要望をまとめておきます。
  • backing fieldはmdPrivateにして欲しい
  • privateメンバーもmdPrivateにして欲しい
  • internal / privateクラスであってもpublicメンバーはmdPublicにして欲しい
といったところでしょうか。

2014年6月9日月曜日

艦これ 司令部室について

F# 談話室の15回に参加し、艦これ 司令部室について発表してきました。これに合わせてGitHubでリポジトリも公開しました。
COMの素晴らしさを力説し、また布教してきました。比較的に好感触でした。
たいしたことをは書いていませんが、発表に使ったプレゼンテーションはこちら


2014年3月12日水曜日

WinFormsのTextBox ControlでのIME制御

WinFormsのTextBox Controlの話題です。とても基本的なControlですが落とし穴がありました。IMEで漢字変換中にフォーカスを失うと未確定文字はそのままIMEが持って行ってしまいます。 WinFormsがこのような挙動をすることを知らず何も制御していなかったために、自作のアプリケーションがクソ呼ばわりされる事態に陥ってしまいました。 とても悲しかったのでTextBoxを派生して挙動を改良してみました。 IMEが開いているかどうかはImeContext.IsOpen()で調べることができますが、そのあと同じハンドルを使用して処理することになるため、IsOpenは使いませんでした。