Siv3DでDxLibのLoadDivGraphみたいなことをする
久々にブログを書く話題がこんな小ネタというのもなんですが、地味に求められているらしいので書いておきます。
DxLib の LoadDivGraph 関数
皆さんご存知 DxLib には、指定した画像を適当なピクセル数で分割して読み込む、LoadDivGraphという関数が存在します。
関数名がゴミとかそういう話もしたいのですがグッとこらえて先に進むと、どうも gif の代わりにアニメーション用の連続した画像を格納するときとかに便利らしく、Siv3D でもそんな関数がほしいという声が散見された[要出典]ので、前に書いた Siv3D での実装を載せたいと思います。
方針&ソース
Image を読み込んで、Image::clipped 関数で指定された大きさのピクセルを切り取っていき、それぞれをTextureに変換していく感じです。
こういうところで step(Size) とかが使えると気持ちいいですね。
ちなみにコメント中の RVO とは Return Value Optimization の略で、関数の戻り値のコピーを消す最適化のことです。C++17 からは絶対にこの最適化が行われるようになります(詳しくはこのへんとかこのへんを読んで下さい)。
// image を大きさ size の画像に分割 Array<Texture> makeDividedTexture(const Image& image, const Size& size) { // 分割数 const Size numDiv{ image.size.x / size.x, image.size.y / size.y }; Array<Texture> result; result.reserve(numDiv.x * numDiv.y); for (auto&& p : step(numDiv)) result.emplace_back(image.clipped(size * p, size)); // RVO で消える return result; }
…とかいう事を書いている最中に、Siv3D の作者様から
必要なのはこのような関数ではないですか? https://t.co/PtIeDIAkNd Texture を複数作るのは実行時性能的に不利です。Texture から TextureRegion を作れば、作成されるのは 1 つの Texture で済みます。
— Ryo Suzuki (@Reputeless) 2017年6月12日
複数 Texture にすれば、mip フィルタ時に隣接する色の影響を受けずに済んだり、repeat できたりという、TextureRegion にはないメリットがあるので、必要になることもあるかもです。
— Ryo Suzuki (@Reputeless) 2017年6月12日
このようなご指摘をいただきました。というわけで、特に「読み込んだ画像を編集したい!」のような場合以外は、TextureRegion を使用した
(via Ryo Suzuki)
のような関数/コードを書くのが良いかと思われます。
結論
- 昔書いた脳死コードを適当に人に上げない。
- Texture の編集を特にしない場合、画像の一部分だけを扱いたいなら TextureRegion を使おう。
以上です。
Siv3Dにおいて一部ビルドでアライメントの関連のランタイムエラーが出たときの備忘録
Siv3DでQuaternionを扱う際にハマった場所があったので備忘録。
問題
問題は「Siv3DにおいてQuaternionなどをメンバに持つ派生クラス
を基底型のポインタ
に突っ込むとx86ビルドで死ぬ場合がある」というものです(自分はx86、Releaseビルドでのみ発生しました)。
具体的には以下の様な例です(このコードが死ぬ保証はありません)。
#include <Siv3D.hpp> #include <memory> class A { public: virtual void draw() const = 0; }; class B : public A { public: void draw() const override { Plane(5.0, q1 * q2).draw(); // ここで死 } private: Quaternion q1 = Quaternion::Yaw(45.0_deg), q2 = Quaternion::Roll(30.0_deg); Plane p = Plane(10); }; void Main() { std::shared_ptr<A> ptr; ptr = std::make_shared<B>(); while (System::Update()) { ptr->draw(); } }
解決方法
あまりに不可解だったので、Twitterで Siv3Dの作者さん にお聞きしたところ「アライメント関連の問題だと思われるので class B に alignas(16) を付けてみて欲しい」という回答をいただき、やってみたのですが、上手く動きませんでした。
しかし、その後std::make_shared
がアライメント指定されたメモリ確保に未対応なのではと思いあたり、アロケータを自作することにしました。
ここでは、Siv3Dにはアロケータを考慮したmallocであるAlignedMalloc
関数が用意されていることを教えていただき、それを使うことで多分結構楽出来ました。
というわけで以下の様な関数を作ることで解決することが出来ました。
template <typename T, typename... Args> auto alignsafeShared(Args&&... args) { auto* p = AlignedMalloc<T>(1); if (!p) throw std::bad_alloc(); new(p) T(std::forward<Args>(args)...); return std::shared_ptr<T>(p, AlignedFree); }
(ちなみにnew(p) T
の部分を*p = T()
にしたら動かなかったので、理由が分かる方は教えて頂けると幸いです)
追記: Twitterで「Mallocだけでは仮想関数テーブルが生成されておらず、operator =
の呼び出しで不正なメモリを参照してしまうから」と教えていただきました。ありがとうございます。
というわけで、上記の例にこれを適応させた最終結果がこうなります。
#include <Siv3D.hpp> #include <memory> class A { public: virtual void draw() const = 0; }; class alignas(16) B : public A { public: void draw() const override { Plane(5.0, q1 * q2).draw(); } private: Quaternion q1 = Quaternin::Yaw(45.0_deg), q2 = Quaternion::Roll(30.0_deg); Plane p = Plane(10); } template <typename T, typename... Args> auto alignsafeShared(Args&&... args) { auto* p = AlignedMalloc<T>(1); if (!p) throw std::bad_alloc(); new(p) T(std::forward<Args>(args)...); return std::shared_ptr<T>(p, AlignedFree); } void Main() { std::shared_ptr<A> ptr; ptr = alignsafeShared<B>(); while (System::Update()) { ptr.draw(); } }
ちなみに、現在開発中の OpenSiv3D にはこのalignsafeShared
と大体同じ機能を持つMakeShared
と、その生ポインタ版であるAlignedNew
が用意される予定だそうです。
SuperCon 2016参加記
今年は幸運にも SuperCon 2016 の本選に参加してきたので、それの所感と経緯を書きたいと思います。
What is SuperCon
正しくは SuperComputingContest で、東工大と大阪大学が共同で開催している大会で、SuperConを使って規模のデカイ問題を解こうぜ!
みたいな大会のはずです。
結果
とりえあず結果だけ先に書いてしまえば、全20チーム中12位という結果に終わりました。
覚悟していたよりは良い結果だったのですが。色々悔しさが残っています(後述)
予選
予選開始前
まずはじめに、僕はプログラミングを4年間やっているのに競プロができないという致命的な問題がありました。
どのくらいかというと、初めてdijkstraを書いたのがこの記事を書く1週間前くらい、というレベルです。
というわけで、最初は割と「何かの間違いで本選出れたりしないかな」みたいな気分でいました。
予選開始
そんな中、 予選問題 が公開されました。
ここで一応簡単に予選問題について解説すると、
6x6の魔法陣に15個の虫食いがあるからできるだけ高速に埋めてね
という問題です。
問題公表から申し込み締め切りまでは17日間あり、その2週間ちょいでコードを書く必要があります。
こんなシンプルな問題ですが、僕は最初パっと解法が思いつかずに悩んでいて、数日は放置気味にしていました。
しかしパートナーの方が解法への糸口を掴んでくれていたらしく、そちらに最初の実装をお願いしました。
予選おわりまで
そこから1週間目の終わりくらいまではパートナーから実装の解説を聞いておかしいところを話し合いつつ、僕は何もせずにパートナーが頑張ってくれていました。
ちなみにこの時点で大体まとまった戦略は
魔法陣の各列に対してその列で使う可能性のある数をbit列で保持しておき、それの論理積を取って各マスの候補をさらに絞り込んでから再帰で全探索
というものでした。
しかし、2週間目に入った時にパートナーの方から「実装が詰まったから手伝ってくれ」というヘルプが飛んできました。
ところが、ソースをもらったところ、コードがグチャグチャでまず問題が発生しているコードの位置を特定できなかったので、基礎の考え方はそのままで僕の方でコードを1から書き直すことにしました。
そんなこんなで2週目の土日までにはコードが完成しました、が、重いテストケースで実行時間が1時間を突破してしまい、これはアカンということで最後数日で最適化を始めました。
そこで、速くなりそうな部分を探したところ、各マスの候補から全探索を行う際に色々と無駄なループが走っていることがわかり、割と気合でコードを書いて対策しました。
すると、実行時間が軽いテストケースでは200μs、重いテストケースでも2~7ms以内に収まるようになり、希望が見えてきました。
しかしこのソース、本来大会側で用意してあるヘッダーで入出力と時間計測を行わなければいけないのですが、それを完全に見落としており、最後の最後で慌てて修正しました。
あとは適当にコメントつけたりしてから提出しました。締め切り前日だったので結構危なかったです。
一応書いたソースは ここ に載せておきます。
別の出場チームの話を聞いたり、Twitterを見たりしたら皆実行時間が短くて大分ヒヤヒヤしたり、予選の結果発表が伸びたのをネタにしたりしていましたが、無事予選通過することができました。
しかし、実は今回の SuperCon はJOIの夏季ゼミと日程が丸かぶりしていたために、昨年の優勝校含む強豪学校が SuperCon に参加しておらず、当初の考え通り「何かの間違いで通過」に近かったかもしれません。
どちらにせよ通過出来たことは単純に嬉しかったです。
予選までの所感
今思うと最初半ば諦めていたとはいえとりかかり始めるのが遅く、完成したソースも最適化が微妙でまだ結構定数倍高速化できる部分がありました。
あとは、SuperConは基本的にC言語オンリー & 実行がUNIX環境のため、Windows & C++ ゆとりの僕には大分辛かったです。このために仮想マシン上にUbuntuでテスト環境を構築するはめになりました。
このC言語制限は本選でも共通だったため、色々と消耗しました。
本選
最初に
SuperCon の本選は東は東工大、西は阪大に予選を通過した20チームが集まり、5日間にわかって問題を解く形で行われます。
いつもは東と西で10チームづつ選出されるらしいのですが、今年は前述の夏季ゼミと被ったこともあってか東側の募集が少なかったらしく、東が8チーム、西が12チームという配分に別れました。
というわけで僕たちは東8チームのうち1チームとして5日間東工大に通いました。このうち、問題を解くのは4日間で、最後の日は表彰式と懇親会でした。
1日目
1日目は午前に集合したのちに、偉い方の挨拶、各チームの紹介などがあり、午後から東工大のコンテスト環境や回答提出方法の解説、問題の発表が行われました。
東工大では端末として iMac が用意されており、OS X と Windows がダブルブートできる環境だったのですが、本選では OS X の方を使用しました。
ここでマウスが Magic Trackpad で無かったり、OS X のバージョンがEl Capitan ではなく Mavericks だったりして高まっていたテンションが少し下がりました(これはむしろ良かったかもしれませんが)
本選問題は簡単にいえば
頂点がn個で次数がdの単純正則無向グラフの ASPL(総最短経路長) ができるだけ小さくなるように経路を求めよ
という問題です。
これを各チームに割り当てられたスパコンのノードを自由に使って解くのですが、今回のスパコンには東工大の TSUBAME が使用され、TSUBAME の特徴である GPU を CUDA C でいかに上手く動かすか、という感じになっていました。
一通りの説明が終わった後は、問題を解く時間となったのですが、まったく解法が思いつかなかったので補助用のシェルスクリプトを書いたりしてUNIXに慣れていました。
パートナーの方は何か解法を思いついたらしく、2人で相談しましたが、僕が上手く実装できる気がしなかったので、予選と同じく最初の実装をパートナーにお任せしました。
2日目
この日は朝の電車内で色々考えた結果、少し前に TSP について調べた時の手法が使えるのではないかという考えに至り、「ランダムでグラフを生成してから、更にグラフのランダムな2辺を取って入れ替える事を繰り返す」という手法を試して見ようと思いました。
2日目は15時から16時の強制休憩タイムを除き、9時から20時まで問題を解ける時間となっていましたが、この日でランダムなグラフの生成を書くことができました。
またランダムなグラフ生成における閉路判定に Uinon Find が必要だったのですが、僕はパッと書ける自信が無かったのでパートナーに書いてもらいました。
パートナーは競プロを比較的真面目にやっていて、データ構造に関する知識があったのでこういう場面ではありがたかったです。
この時点で、この手法に関して「これはイケる」という雰囲気を感じており、パートナー側と話し合って、2人で別の解法でプログラムを書こうという作戦にしました。
ちなみに、本番が終わった後に他のチームから指摘されて気づいたのですが、入力例に対するランダムなグラフの生成はサンプルプログラムとして大会側から配布されており、1日目にその説明もありました。
つまり、ランダムなグラフの生成は特に理由がなければ、手書きする必要がないようになっていたのです。
しかし、僕はそれを完全に失念しており、この2日目をグラフ生成に使ってしまいました。
これが最終日に時間が足りない直接の原因になり、今も凄まじく後悔している一番のミスです。
また、強制休憩タイムでは関西会場とつながったテレビ会議システム上でお絵かきしりとりをしていました。楽しかったです(小並感)
あとはGoogleの方からNougat入りのチョコレートの差し入れがあり、ありがたくいただいていました。
3日目
3日目も2日目と同じように、強制休憩タイム以外は進捗の時間として使えました。
この日は「2つのランダムな辺を選択して入れ替え、ASPLを計算した後に小さい方を取る」という部分の実装をしました。
ASPLを求める方法については事前に幾つか手法の紹介をされていたこともあり、この部分の実装は午前のうちに終わった…とおもいきや、ASPLの値が盛大におかしいことになり、昼ごはんを食べるまでずっと悩んでいました。
しかし昼ごはんを食べた後にすぐ、辺の入れ替え後に正しいグラフになっているかの判定でUnion Findを使っているのに、Uinon Find用のtreeが更新できていないことに気づき、修正が面倒くさそうなので手法を変えて実装しました。
食事は偉大です。
ちなみにこの「辺入れ替え後のグラフ判定」ですが、そもそも辺を入れ替えたあと閉路で無かった場合は ASPL が INF になるようになっているので、そもそも必要でないことに4日目で気が付きました。
食事は偉大でも、食べる側の人間の頭は偉大から程遠い場所にあったようです。
あとは、そもそも辺をswapする際の動作が間違っていたせいでinvalid graphになる問題なども発生して、午前に感じていた余裕は吹っ飛びました。
最後1時間位はASPLの計算に使用したフロイド・ワーシャル法の並列化方法が思いつかずに悶々としていました。
これに関しては家に帰ってからきちんと考えました。
4日目(午前)
4日目は、コーディングできるのは午前と本番である13時から15時の間のみでした。
朝になった時にパートナー側で試していたプログラムが結局ダメであった事を告げられ、僕のプログラム一本で行くことになりました。割とキレました。
しかし、前日の夜に足りない頭を絞った結果、フロイド・ワーシャル法の並列化がめちゃくちゃ楽な事に気づいたので、ぱっと実装…と言いたかったのですが、それまで全く CUDA に触れていなかったため、スレッドブロックの割り当てなどでちょっと手間取りました。
更に、これで完成…と思いきや、フロイド・ワーシャル法の後の配列の総和を求める部分をCPUでやると、メモリ転送コストで時間がCPU実装とそんなに変わらないという事になってしまっていました。
というわけでGPU側で配列の総和を求めるプログラムが必要になったのですが、これも大会側がCUDA向けのサンプルとして用意してくれていましたので、それを使おうとしました。
しかし、この総和の算出部分がなぜか上手くいかず、謎を抱えたまま昼になってしまいました。
本番
昼の後、本番がはじまりました。
本番は、入力が9個与えられ、それぞれの入力に対するできるだけASPLが小さい経路を2時間の間に求めて提出する形式で、その2時間の間はプログラムの修正が自由です。
CPUのみを使うプログラムではASPLの算出に時間がかかり、与えられた頂点数が大きい場合、辺の入れ替えを効果がでるまで繰り返す事ができないのがわかっていたので、パートナーにとりあえずの提出を任せ、僕はGPUでの総和計算でのバグを治すことに全力を注ぐことにしました。
しかし、結果だけ言ってしまえば、総和計算のバグを取ることは出来ませんでした。
このバグの原因は未だにわかっていません。
CPUの方でも超点数が少ない場合は試行回数を増やせるので、かなり手応えのある結果を出せる事が確認済みだったのですが、運の悪いことに、本選の問題の多くは頂点数が大きめに設定されており、最終的には全く良い結果が出せませんでした。
更にその後に2日目のミスに気づき、パートナーとともにただひたすらに後悔していました。
本番後は大会側から各チームへのインタビューがあり、チーム名の由来やメンバーの得意技を、問題を解いたアルゴリズムを聞かれました。
得意技に関しては、全く思いつかなかったので「OSデータ破損芸」と言ったらそのまま採用されてしまいました。
5日目
5日目は表彰式と懇親会でした。
表彰式ではまず主催者やスポンサーからの挨拶、再度各チームの紹介などがあり、そこから結果発表と入賞チームや奨励賞受賞チームへの賞状や副賞の授与がありました。
結果発表に本番2時間中の各入力での5位までの順位の変動がわかる動画が使われており、面白いなと思って見ていました。僕らのチームは一回も動画の中に出てきませんでした。
最終的に僕らの結果は上記の通り12位でした。
その後は懇親会があり、じゃんけん大会をしたりしました。
本番で使われなかった運をじゃんけん大会で使った気がする程度には勝ち、いくつか景品を戴きました。
あとは部屋でダベってから帰って爆睡しました。
全体を通しての感想
予選でも本選でもそうですが、配布されたものをしっかり確認しなかった結果、丸々1日損する事になったので、もっと気をつければよかったというのが一番の反省ポイントです。
あとは、慣れないシェルスクリプト、Vim、C言語 & CUDAについてチューターさんに質問を大量に投げてしまったのですが、毎回しっかり回答をいただけたのは非常にありがたかったです。
まああとはGPUの総和バグの原因が未だに分かっていないのが気がかりです。家にCUDAを実行できる環境がないのでもう直せないのがなおさらですね。
とりあえず、プロは強いということが実感できたので、来年も本選出場してもっと上を目指したいです(KONAMI)
Siv3D上で動作するプロセス間通信ライブラリを作った
作っているものの関係でSiv3Dで動作するプロセス間通信ライブラリを組んだので、それの使い方とか概要を書いておく記事。
概要
名前はAzaika siv3D extention library、頭文字を取ってAselです。simple is best。
今のところ、他プロセスの制御とプロセス間通信機能があります。
しかし、他プロセス制御部分はちゃんと動いてないっぽいので、今回はプロセス間通信の方について書こうかと思います。
使い方
まずgithubからソースを落とします。
そしたら落としたソースの中にあるファイルを適当な場所にコピーし、Aselフォルダ内のcppを全てプロジェクトに追加します。
あとはAsel.hをincludeすれば使えるようになります。
使い方は適当なコードを見たほうが速いと思うのでそうします。
とりあえずサーバー側のコード
#include <Siv3D.hpp> #include "Asel.h" void Main() { const Font font(30); const String sendText = L"Kitty on your lap"; bool hasSent = false; asel::Server server(L"aselTestServer"); if (server) { server.start([&](asel::File& pipe) { pipe.write(sendText); }); } while (System::Update()) { if (server) { font(L"Server: \n" + server.getName() + L"\nの構築に成功").draw(); if (hasSent || server.hasConnected()) { server.update(); hasSent = true; font(L"テキスト: " + sendText + L"\nを送信しました").draw({ 0, 170 }); } else font(L"クライアントからの接続待機中").draw({0, 170}); } else font(L"サーバーの構築に失敗").draw(); } }
そしてクライアント側のコード
#include <Siv3D.hpp> #include "Asel.h" void Main() { const Font font(30); String readStr = L"サーバーへの接続に失敗"; auto client = asel::connectServer(L"aselTestServer"); if (client) { auto str = client.read(17); if (str) readStr = *str; client.close(); } while (System::Update()) { font(readStr).draw(); } }
こんな感じです。
コードだけで大体の流れは掴めるかと思います。
実行してみれば更に流れが掴めると思います。
…なんていう適当な紹介はこれまでにして、もうちょっと詳しく見ます。
まずサーバー側の
server.start([&](asel::File& pipe) { pipe.write(sendText); });
この部分。
Server.start()
は引数にasel::File&
型を取り、void
を返す(何も返さない)関数を引数に取ります。ここでラムダ式を使うと便利ですね。
引数に渡した関数は、クライアントからの接続があるときにServer.update()
すると呼ばれます。
asel::File
型については後述。
次にクライアント側の
auto client = asel::connectServer(L"aselTestServer");
この部分。
asel::connectServer()
は引数で渡された名前のサーバー(内部的にはパイプ)に接続し、asel::File
を返す関数です。
さて、この2つに出てきたasel::File
型というのは、誤解を恐れずいうとServerが持つ内部的なパイプの実体(のインターフェース)です。
asel::File
型にはFile.read(int size)
とFile.write(const String& str)
関数が用意されており、
File.read(int size)
はsize文字分の文字列を読み込みOptional<String>
を、
File.write(const String& str)
はパイプにstrを書き込みbool
を返します。
これが大体の基本です。
しかし、一つ注意しなければならない点があります。
実行してみた人は気づいたと思いますが、asel::Server
のコンストラクタやasel::connectServer
の引数に渡したサーバーの名前というのは簡略化されたもので、WinAPIの仕様上実際には先頭に\\.\pipe\
のような文字列が付加されます。
この辺については色々と用意してあるので適当に使ってみてください。
また、ライブラリで用意されているほとんどの関数にはXMLコメントで解説を書いてあります。
ちょっと分からなくなったら見てみて下さい。
最後に
という感じです。
また、このライブラリはまだバグがあったり、仕様が甘かったりする点が大量にあると思うのでそういうのを見つけたらIssuesに上げるか作者Twitterまで言って(そしてあわよくばpull-reqを)下さい。多分直します。
というわけでオレオレライブラリの紹介でした。
もうちょっとだけオレオレライブラリの宣伝をすると、これをWinAPIでも使いたい人はwawlというWinAPIラッパライブラリを公開していますので使ってみてください。
VS CodeでHaskellの簡単な環境を作る
Haskellをやろうと思ったら、Visual Studio CodeがまだHaskellに対応してなかったので、Visual Studio CodeでHaskell環境を整える備忘録。
シンタックスハイライトの追加
初めに、最低限のシンタックスハイライトを追加する。
VS Codeを起動して Ctrl+P を入力し、出てきた入力欄にext install haskell
と入力し、候補にあるHaskell Syntax Highlighting
を選択。
mattn.runnerのインストール
mattn.runnnerとはVS Codeで書いたコードを実行するためのキーボードショートカットを追加できるExtension。
詳細はこちらに
mattn.kaoriya.net
これもVisual Studio Marketplaceに登録されているので、先程と同様にしてext install runner
と打ち、出てきたRunnerを選択。
ここで一旦VS Codeを再起動してinstallを適用。
mattn.runnerの設定
まず、適当なフォルダを作り、haskellを動かすためのbatファイルを書く(今回は%userprofile%
下にvsc_config
というフォルダを作る)。
作成したフォルダにrun_hs.bat
というファイルを作成。
そしたらこのbatファイルを開き、次のように編集。
@echo off if "%~dpnx1" equ "" goto :eof setlocal set tempfile=%date:~4%%time::=% set tempfile=%tempfile:/=% set tempfile=%tempfile:.=% set tempfile=%tempfile: =% ghc -o %tempfile%.exe --make "%~dpnx1" -outputdir "tmp" %tempfile%.exe del %tempfile%.exe rd /s /q "tmp" endlocal
保存したら、VS CodeのメニューからFile→Preferences→User Settingsを開き、settings.jsonに
"runner.languageMap": { "haskell": "%userprofile%/vsc_config/run_hs.bat" }
を追加し、保存。
そしてVS Codeを再起動。
試してみる
これで、VS Codeでhaskellを書いている時に Ctrl+Shift+R を押せばそれを実行してくれるようになる。
テストとして適当なhaskellコードを実行してみる。
きちんと動いてるみたいだ。
しかし、この機能は保存していないファイルには使用できないようなので注意。
自分の活動と作っているものの宣伝【中高生プログラマAdC 9日目】
この記事は中高生プログラマAdC 2015の9日目の記事です。
AdCのためにアプリをPonと作れるプロになれなかったので自己紹介とちょっとした宣伝を書こうと思います。
めちゃくちゃ簡単な自己紹介
Azaika(@azaika_)です。どっかの学校に通う男子学生です。
プログラミングは学校の部活で学んでます。
部としての活動
部としては主に次の2つの活動をしています。
- 競技プログラミング
- ゲーム開発
その他にも3DCGやDTMなどのPCを使った創作活動をしている部員がいます。
しかし、創作物に関しては基本的にチーム開発などはあまり行わず、個々が作った創作物に関して部員同士で相談できる場として機能していると感じます。
自分の活動
さて、部の活動として競プロとゲーム開発を上げましたが、僕はそのどちらも熱心にしてはいません。
では何をしているかというと、ライブラリの開発です。
うちの部では最近まで、ゲーム開発は先代が作ったWinAPIとGDI+を軽くラップしたライブラリを使い続けて来たのですが、
このご時世にGDI+もないだろうということでDirect2Dで書きなおしたり(書き終えたあたりで部での開発用ライブラリがSiv3Dに移行して悲しい)、
その折でWinAPI使いにくい、となったのでWinAPIのラッパーを書いたりしてます(そのWinAPIラッパーはGithubに公開していますので使ってみて感想くれたり、あわよくばプルリクなど頂けると泣いて喜びます)
その間にC++の言語仕様も少しづつ勉強して、最近は新入生向けのプログラミング教育もしてたりしてなかったり。
また最近ではC++だけでなく、Windows10のUWPを開発しようとC#も勉強中です。
あとはほんのちょろっとHTML/CSS勉強したり、Haskell触ったり、Linux(Arch)で遊んだりしてます。
WinAPIラッパーについて
さて、自己紹介だけで終わるのもアレなので、さっき書いたWinAPIラッパーの解説(宣伝)と苦労話でもしようかと思います。
僕の作っているWinAPIラッパーは名をwawl(Windows Api Wrapper Library)と言い、設計思想としては「WinAPIを本家の設計をあまりいじらずC++といえるくらいにする」というのがあります。
そのためあまり構造をいじらず、機能ごとのclass分け、定数のenum class化などが主になります。
具体例を出せば、Processクラスのコンストラクタの引数構造がだいたいWinAPIのCreateProcessと同じです。
ただし、WinAPIネイティブ(?)な型は一切存在しておらず、全てwawl内部の型で完結しています。
また、WinAPIを用いた外部ライブラリとの同時使用も視野に入れ、それぞれのclass毎にWinAPIネイティブである内部型を取得する機能も用意しています(Processで言えば、process.get()でPROCESS_INFORMATION構造体が取得できる等)
これらにより、泥臭いWinAPIをかなり手軽に扱えるようになっています。
サンプル
#include "Window.h" #include "FileSystem.h" #include "Input.h" #include "Using.h" void wawlMain() { wnd::RootWindow window( L"Kitty on your lap", wnd::Prop({ wnd::PropOption::HRedraw, wnd::PropOption::VRedraw }), { 640, 480 }, wnd::Style::Overlapped ); fs::Process proc; //Window表示 window.setShowMode(); //左クリックしたらメモ帳起動 window.on(Msg::LClick, [&](UintPtr lp, IntPtr rp) { if (!proc) proc.open(L"notepad"); return window.defaultProc(Msg::LClick, lp, rp); }); //右クリックしたらペイント起動 window.on(Msg::RClick, [&](UintPtr lp, IntPtr rp) { if (!proc) proc.open(L"mspaint"); return window.defaultProc(Msg::LClick, lp, rp); }); while (Sleep(10), true) { //Tが押されたら起動しているアプリを終了 if (kb::getState(KeyCode::T) && proc) proc.terminate(); //Esc押したら終了するかユーザーが選択 if (kb::getState(KeyCode::Escape) && mb::show(L"wawl", L"Do you shutdown this application?", mb::Button::YesNo) == mb::Result::Yes ) return; //Windowが存在すればUpdate、存在しなければ終了 if (window) window.update(); else return; } }
簡略化のためにエラー処理は省いてあります。
このコードを実行すると、真っ白なWindowが出てきてソースのコメントに書いたままの挙動をします。
ちなみに、描画等に関してはWinAPIの範疇ではない(DirectX等の範疇)としてサポートしていません。
これに関しては@yuki74wさんがDirect3Dラッパーを作ってくれると言ってくれていますし、僕もDirect2Dラッパーを作ろうかと思っています。
ここからは苦労話
WinAPIで何が一番辛いかというと資料集めです。ある程度メジャーなところまでは日本語の資料があるのですが、それ以上になると英語のMSDNを巡礼し始め、知らぬ間にロシア語をページ翻訳して読む羽目になったりします。
これで一番よくあるのが定数の詳細等です。例えばキーコード定数にVK_CRSELという定数があるのですが、これが何かというとCursorSelectという昔のIBMキーボードについていたもの用の定数だったりします。
あとは、ProcessやFileでのアクセス制限や権限関係の定数などのファイルシステムに踏み込むかのような内容が含まれたものもあり、非常に混乱しました。
あとC++的なところの辛みで言えば、参照の寿命問題などに直面して2週間潰したり、WinAPIの仕様との兼ね合いでclassの構造を工夫せざるを得なくなったりしました。
最後に
初AdCだったので結構怯えながら書いてます。
あとプログラミング(IT)関連の方は是非Twitterでフォローしていただけると嬉しいです。
長い駄文でしたが、ありがとうございました。
namespaceのエイリアスでちょっと気をつけること
コードを書いていて少し詰まった
#include <iostream> namespace A { namespace B { void f() { std::cout << "called f" << std::endl; } } void g() { //何かしらの処理 } }
こんなコードを書き、gはA::gとして呼び出したいが、fはB::fとして呼び出したい、みたいな事になった。
こういうときusing namespace A;はダメだ、using namespace A::B;も使えない。
そこでこんな2つのコードを書いた。
using B = A::B; using namespace B = A::B;
しかしこれはコンパイルエラーになる。どうやらusingでのエイリアスはnamespaceに対してはできないらしい。
ではnamespaceをエイリアスするにはどうすればいいかというと
namespace B = A::B;
のようにすれば良い。
しかしこれはどうもわかりにくい気がするなといった次第。