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は異なる関数として扱うためテンプレート展開に失敗するためです。