maminusのメモか何か

2017-05-07

clang4.0.0使ってみた

| 22:04

普段ビルドに使っていない方のPCにclangを入れようとしたら結構バージョンが上がっていた。

忘れないようにメモっておく。

環境

環境はこんな感じ。

  • Windows7 Ultimate SP1 64bit
  • MinGW32 5.3.0
  • Clang 4.0.0

インストール

mingwとclangは両方ともインストーラを公式サイトからダウンロードしてインストールウィザードに従うだけで終わった。

環境変数

コンパイルするときの環境変数はこんな感じで。

set MINGW=C:\MinGW

set INC_TOP=%MINGW%\lib\gcc\mingw32\5.3.0\include

set PATH=%PATH%;%MINGW%\bin

set C_INCLUDE_PATH=%MINGW%\include

set CPLUS_INCLUDE_PATH=%C_INCLUDE_PATH%;%INC_TOP%\c++;%INC_TOP%\c++\mingw32

ちなみにMinGWへパスを通していないとldコマンドの実行でコケる。

コンパイル時の注意点

そのままだとVisualC++前提でコンパイルするらしく、「i686-pc-windows-msvc」というtargetでコンパイルしようとしてmingw用のヘッダファイルでコケてしまっていた。

解決策として「--target=i686-pc-windows-gnu」をコンパイル引数に指定すると大丈夫だった。

たとえば以下のような感じになる。

>clang++ -std=c++11 --target=i686-pc-windows-gnu -o sample.exe source.cpp

2016-03-12

clangでWindows SDKを使ってみた

| 07:52

Windows7環境でSDKを使ったプログラムを開発しようとしてみた。

使ったバージョンは以下の通り。

なお、以前に書いた「clang+boostWinアプリ開発」とかの記事*1を前提としているので注意されたし。

最終的にはここに書いたこととmakefileの内容がすべてではあるけど、以前の記事で書いたことは説明を省いている。


SDKを使う際には、MinGW側で用意してくれていたヘッダからWindows SDK側のヘッダに移行するので色々いじる必要がありそうだった。

以下やったことの内容をメモ。


SDKのヘッダファイルを無理やり書き換えてコンパイルを通したもの

driverspecs.h

コンパイルエラーになるので、コンパイラへのアノテーション関連マクロを無効化。

以下のマクロ定義を空っぽに変更。マクロ自体は残して「__$drv_group」などの定義部分をコメントアウトした。

  • __drv_deref(annotes)
  • __drv_in(annotes)
  • __drv_in_deref(annotes)
  • __drv_out(annotes)
  • __drv_out_deref(annotes)
  • __drv_when(cond, annotes)
  • __drv_at(expr,annotes)
  • __drv_fun(annotes)
  • __drv_ret(annotes)
  • __drv_arg(expr,annotes)

※:__drv_when、__drv_atに関しては__drv_declspec部分は残して__$drv_groupだけコメントアウト

  こういう感じ

	#define __drv_when(cond, annotes)										\
	  __drv_declspec("SAL_when(" SPECSTRINGIZE(cond) ")") //__$drv_group(##__drv_nop(annotes))
	#define __drv_at(expr,annotes)\
	  __drv_declspec("SAL_at(" SPECSTRINGIZE(expr) ")") //__$drv_group(##__drv_nop(annotes))
oaidl.h

一か所「_VARIANT_BOOL bool」というメンバ名でコンパイルエラーになるので、メンバ名を「bool_」に変更する。

$(MINGW)\includeにも同名のファイルがあるけど、内容が違うので気にしない。

winnt.h
  • 「LONGLONG」および「ULONGLONG」がdouble定義になっている箇所を「long long」、「unsigned long long」に変更する
wtypes.h

MinGW側のヘッダがwtypes.hでVARIANT_BOOLを定義していて、さらに_VARIANT_BOOLを参照していて衝突するので、以下のように変更する。

//#define _VARIANT_BOOL    /##/	// Windows SDKでは、このように定義されているものを

typedef VARIANT_BOOL _VARIANT_BOOL;	// こちらに変更する
excpt.h

このファイルは$(MINGW)\includeに入っているものを改造。

変更内容は「#include <windef.h>」を削除。

Windows SDK側のwindef.hがwinnt.hをincludeしてるので、excpt.hがEXCEPTION_DISPOSITIONを定義する前に参照してしまってコンパイルエラーとなる。

windef.hは他でもincludeしているので単純に削除して依存関係がうまくいくように調整。

sal.h

ファイルが無いため、ここ*2からもらってくる。

さらに以下の定義が足りないので追加する。(2015/06/26 閲覧)

//C:\Program Files\Microsoft SDKs\Windows\v7.1\Include\winnt.h:363:9: error: unknown type name '__nullterminated'
// typedef __nullnullterminated WCHAR UNALIGNED *PUZZWSTR;
#define __nullnullterminated
#define __nullterminated
//C:\Program Files\Microsoft SDKs\Windows\v7.1\Include\winnt.h:366:9: error: unknown type name '__possibly_notnullterminated'
//typedef __possibly_notnullterminated WCHAR *PNZWCH;
#define __possibly_notnullterminated
//C:\Program Files\Microsoft SDKs\Windows\v7.1\Include\winnt.h:542:19: error: expected parameter declarator
//typedef __success(return >= 0) long HRESULT;
#define __success(expr)
//C:\Program Files\Microsoft SDKs\Windows\v7.1\Include\winnt.h:1201:1: error: unknown type name '__post'
#define __post
//C:\Program Files\Microsoft SDKs\Windows\v7.1\Include\winnt.h:2116:5: error: unknown type name '__maybenull'
#define __maybenull
//C:\Program Files\Microsoft SDKs\Windows\v7.1\Include\winnt.h:8626:5: error: unknown type name '__notnull'
#define __notnull
#define __elem_writableTo(size)
#define __inner_checkReturn
#define __byte_writableTo(size)
#define __pre
#define __callback
#define __drv_nop(annotes)
#define __deref
#define __notvalid
#define __reserved
#define __valid
#define __exceptthat
#define __inner_control_entrypoint(GDI)
#define __refparam
#define  __elem_readableTo(x)
#define __format_string
#define __typefix(x)
//#define SPECSTRINGIZE(x)
//#define PURE
//C:\Program Files\Microsoft SDKs\Windows\v7.1\Include\objidl.h:11280:68: error: C++ requires a type specifier for all declarations
#define __RPC__out_xcount_part(x, y)
//C:\Program Files\Microsoft SDKs\Windows\v7.1\Include\objidl.h:11284:33: error: unknown type name '__RPC__in_xcount_full'
#define __RPC__in_xcount_full(x)
//C:\Program Files\Microsoft SDKs\Windows\v7.1\Include\objidl.h:13063:106: error: expected parameter declarator
#define __RPC__inout_xcount(x)
//C:\Program Files\Microsoft SDKs\Windows\v7.1\Include\objidl.h:13064:103: error: expected parameter declarator
#define __RPC__in_xcount(x)
//oaidl.h:2134:47: error: expected parameter declarator
#define __RPC__in_range(x, y)

追加したDLL

Windows SDKに入ってない?DirectX側から持ってきたらアプリが起動するようになったのは以下のDLLたち。

D3DCompiler_42.dll

d3dx10_42.dll

d3dx11_42.dll

d3dx9_42.dll

Detoured.dll

Detoured.dllってなんぞ?と思ったけど、ファイルのプロパティを見ると「Microsoft Research Detours Package」って書いてある。

おそらくここ*3が公式サイトと思われる。

Detours

Software package for re-routing Win32 APIs underneath applications.

ということで、APIの転送をやっているらしい。


ビルドオプション

-D_M_IX86

MinGW側のwindows.hで定義されていたが、Windows SDKにはdefineされていないので追加。

本当は「600」とかに定義しておいた方がよいかも。

-D_STDCALL_SUPPORTED

Windows SDKのヘッダ数か所で「__stdcall」を使うdefine定義を有効にするために追加。

これがないと「WINAPI」とかがデフォルトの呼び出し規約になってしまう。

(clangってデフォルトは何だっけ?cdecl?)

こいつを定義していないせいでひどい目にあったのだが、それについては後述したい。

-fms-extensions -fenable-experimental-ms-inline-asm

デフォルトだとMS形式のインラインアセンブラコンパイルできないのでコンパイルオプションに付与する。

オプションを付けると以下の警告が出る。

warning: MS-style inline assembly is not supported [-Wmicrosoft]

こちら*4によると

the the asm("...") syntax is part of the C++ standard, whereas Microsoft's __asm { ... }; is not.

ということらしい。ので、当該警告をoffにするのもありかと。「-Wno-microsoft」かな?

自分は当該警告オプションを付けずに他の警告オプションをいくつか付けている。makefileの内容を後で掲載するのでそちらを見てほしい。

-Wl,--enable-stdcall-fixup

WindowsDLLとリンクするときに「@」が邪魔になるのでこのオプションを付与する。

コンパイラごとの命名規則?はここ*5に一覧表が書いてあって分かりやすい。(clangはMinGW列ね)


makefile

以上を踏まえて、例えばmakefileはこんな感じにする。

boostDirectX SDKも一緒にくっつけて、今まで作っていた簡単な画面表示程度のアプリビルドできることを確認した。

CC		= clang
MINGW	= C:\MinGW
DX_TOP	= C:\DirectXSDK
DX_INC	= $(DX_TOP)\Include
DX_LIB	= $(DX_TOP)\Lib\x86
DX_LIBS	= $(DX_LIB)\d3d11.lib $(DX_LIB)\d3dx11.lib $(DX_LIB)\d3d10.lib $(DX_LIB)\dxgi.lib $(DX_LIB)\dxguid.lib
FXC		= $(DX_TOP)\Utilities\bin\x64\fxc
FX_INC	= $(DX_TOP)
BO_TOP	= C:\boost_1_55_0
BO_LIB	= $(BO_TOP)\stage\lib
BO_LIBS	= $(BO_LIB)\libboost_thread-clang32-mt-s-1_55.lib $(BO_LIB)\libboost_system-clang32-mt-s-1_55.lib
WIN_LIB	= C:\Windows\SysWOW64
WIN_LIBS= $(WIN_LIB)\user32.dll $(WIN_LIB)\gdi32.dll $(WIN_LIB)\kernel32.dll
INC		=								\
	-I $(DX_INC)						\
	-I $(BO_TOP)						\
	-I .								\
	-I "C:\Program Files\Microsoft SDKs\Windows\v7.1\Include"		\

LIBS	=					\
	$(DX_LIBS)				\
	$(BO_LIBS)				\
	-lstdc++				\
	$(WIN_LIBS)				\
	d3dx11_42.dll	\

CFLAGS	= -Wall -W -std=c++11 -DBOOST_THREAD_USE_LIB -Wno-c++11-narrowing -D_M_IX86 -Wno-unknown-pragmas -Wno-reserved-user-defined-literal -fms-extensions -fenable-experimental-ms-inline-asm -D_STDCALL_SUPPORTED
LFLAGS	= -static -mwindows -Wl,--allow-multiple-definition -Wl,--enable-stdcall-fixup -Wl,-Map=mapfile.map
SRCS	= 					\
	main.cpp				\

OBJS	= $(SRCS:.cpp=.o)
TARGET	= sdk-test.exe

.PHONY: all clean env delete_objs delete_target

all: CFLAGS += -O2
all: $(TARGET)

debug: CFLAGS += -g
debug: LFLAGS += -g
debug: $(TARGET)

$(TARGET): $(OBJS) $(BLOBS)
	$(CC) -o $@ $(OBJS) $(LFLAGS) $(LIBS)

env:
	set PATH=%PATH%;$(MINGW)\bin
	set C_INCLUDE_PATH=$(MINGW)\include;$(MINGW)\lib\clang\3.2\include
	set "CPLUS_INCLUDE_PATH=%C_INCLUDE_PATH%;$(MINGW)\lib\gcc\mingw32\4.6.2\include\c++;$(MINGW)\lib\gcc\mingw32\4.6.2\include\c++\mingw32"

trigger-help:
	$(DX_TOP)\help\windows_graphics.chm

clean: delete_objs delete_target

delete_objs:
	del /f /q $(OBJS) $(BLOBS)

delete_target:
	del /f /q $(TARGET)


.SUFFIXES: .cpp .o

.cpp.o:
	$(CC) -c $(INC) $(CFLAGS) -o $*.o $<


_STDCALL_SUPPORTEDが無いせいで呼び出し規約不一致によりアプリクラッシュした件

この項では参考情報としてトラブルシューティングのステップを順に書いていきたい。

DLL側とアプリ側で呼び出し規約が一致しない場合にどのような現象となるのか報告しておきたい。*6


現象

前提としてWindows SDK側のwindef.hなどには、_STDCALL_SUPPORTEDなら「#define WINAPI __stdcall」みたいな定義がある。

のだが、そんなことになっているとは知らず最初は_STDCALL_SUPPORTEDをdefineしていなかった。

そうすると

と呼び出し規約が一致しなくなる。

結果的にはアプリがクラッシュするわけだけど、以下のように若干ややこしい見え方をしていた。

ちなみに上記は関係なさそうな処理をどんどん削除していって現象が再現する最小のコードにした状態で確認した。


不具合発生箇所の切り分け

現象面だけ見てもなんだかよくわからないのでWinDbg経由でアプリを起動してどのあたりで死んでいるのか見てみた。

ただ、直接死んだファイルアクセス箇所は別段異常が見当たらない。

仕方がないので呼び出し元に戻ってみるが、おかしなところは見つからない。

ここで、時刻情報もおかしい事実を思いだした。fstreamに現在時刻を出力しているのだが、時刻がめちゃくちゃな値になっていた。

時刻情報はWindows APIを呼び出して取得している。そして今回変更したのはWindows SDKの切り替えだけ。

ということは、API呼び出しで何か良くないことが起きている、という推論にたどり着いた。


Windows API呼び出し箇所の確認

クラッシュ箇所のファイルアクセスよりも2関数ほど前に該当のAPI呼び出しがあった。

スタックの内容を破壊したのなら関数から戻る際にクラッシュするのが普通だと思う。

イメージ的には以下のようになるはず。

void stack_bomb(void)
{
	char	buf[128];

	api_oops();		//この関数内部でスタックの内容を壊したとする。ケース(1):この関数からstack_bomb()に戻る過程でクラッシュする

	funcA();		//funcA()用にスタックを伸長するのでfuncA()関連でクラッシュするケースは少ないと思う

	buf[xxx];		//もしかしたらローカル変数もおかしいことになっているかも?

	funcB();		//同様にここでクラッシュするケースも少ないと思う。が、今回はこの内部でクラッシュした

	return;			//ケース(2):stack_bomb()から戻る過程でクラッシュする
}

スタックの内容を見たが、特に壊れている雰囲気はない。

funcB()で死んだ理由を逆アセンブル結果で確認すると、引数で渡したポインタ変数へのアクセスで死んでいる。

ポインタを見ると確かにおかしなアドレス値になっている。

本来ならここで正しいアドレス値がスタックのどこかに入っていないか確認すればよかったかもしれない。

ただ、STLが内部で保持しているデータ構造なので確認が面倒ではないかと思う。どちらにしろ当時は思い浮かばなかった。


ひらめきの訪れ<仮説を立てる>

それで、ここからは完全に直感なんだけど「もしかしてスタックポインタずれてない?」と思い浮かんだ。

「なぜ?」と聞かれても困るが、それまでの状況証拠を見ているときに突然脳みそが提示してきた。もしかしたら「スタックの内容は壊れてない。スタックポインタもそれっぽいアドレス。なら少しだけアドレスがずれているのかも?」みたいな発想なのかもしれない。

自分の持論として「エンジニアの直感は75%の確率で正しい」という謎の持論があるので、まずは真偽を確かめることにした。


仮説の検証

やり方は単純。上記のapi_oops()、funcA()、funcB()の呼び出し前後でスタックポインタ値が変化するかどうかを見ればよい。

WinDbg上でレジスタを表示しながらステップ実行してみる。

そうすると案の定、api_oops()だけ呼び出し前後でスタックポインタ値が少しずれていた。(具体的には4バイト)


真の不具合内容を確認

ここまで来ればあとは単純なパワー作業だ。機械語の逆アセンブル結果とにらめっこしながらスタックポインタがずれる原因を確認すればよい。

で、1命令ずつ確認していった結果、呼び出し元と呼び出し先でスタックポインタの扱いが一致してないことが分かった。


不具合混入の原因を調査

そうと分かればあと一息。この手の問題は呼び出し規約と決まっている。

Windows APIとclangの呼び出し規約を確認s・・・ん?あれ??

そもそもWINAPIってstdcallだよね。って話。昔調べたぞ。WinMain()とかに付けるあれだ、あれ。

ここでようやく「Windows SDKのヘッダファイルがWINAPIをどう定義してるか」って話にたどり着いた。(長い)

あとはgrepしてWINAPIの定義を見つけ出し、「_STDCALL_SUPPORTED」が定義済みの時にstdcallになる、ということを確認した。


感想

ということで、_STDCALL_SUPPORTEDを定義しないとクラッシュするね。って話になる。

最初にAPI呼び出しでクラッシュしてると分かっていればもっと早く答えにたどり着いたかもしれないけど、一見別の関数で死んでいるように見えたので遠回りしてしまった。

スタックポインタ値も「おおよそスタックポインタ値の範囲にある」というのはすぐわかるが、「4バイトずれている」というのは関数呼び出しの前後で比較しない限り分からない。

呼び出し規約が違う、というのも逆アセンブル結果で比較する程度しかないのかな。と思う。もしかしたらもっとすぐ確認する方法があるかもしれないけど。

2016-01-15

MinGW製QtDLLをclangから呼び出せなかった話

| 23:05

今回は単なる失敗談。

  • clang 3.2
  • Qt 5.5.1

やろうとしたこと

Windows PCにQt Creatorをインストールしたので、サンプルアプリをclangでビルドして実行しようとした。

QtはMinGWビルドされていたからABIの不一致がちょっと心配だった。

色々いじって警告を出しながらビルドは通った。でも、実行したらmain()関数の1行目でアクセス違反して落ちてしまった。


アクセス違反の症状

だいたいこんな感じだった。

ここまで確認して以前Windows SDKアプリビルドした時に、呼び出し規約が一致していないせいでアクセス違反した時と雰囲気が似ていることに気が付いた。*1

アプリclang
DLLMinGW

コンパイラが違うので呼び出し規約が違う可能性があると思った。


デバッガで確認した内容

確認したのは主に逆アセンブル結果とスタック上の値。

やはり呼び出し規約が違っているようだった。

コンパイラ第一引数スタックの巻き戻し
MinGWECXレジスタ経由関数から戻った際にスタック調整
clangスタックスタック調整なし

裏取り

で、ここまで調べて「そういえば、Wikipediaの呼び出し規約ページを*2見ていた時に同じような説明見たな」と思いだした。

thiscall

(中略)

On the Microsoft Visual C++ compiler, the this pointer is passed in ECX

and it is the callee that cleans the stack, mirroring the stdcall convention

used in C for this compiler and in Windows API functions.

When functions use a variable number of arguments,

it is the caller that cleans the stack (cf. cdecl).


ん?あれ?VCの話だぞ。

あと、スタックの後片付けは呼ばれる側になってる。

Microsoft社のサイト*3も見てみた。

The __thiscall calling convention is used on member functions

and is the default calling convention used by C++ member functions

that do not use variable arguments. Under __thiscall,

the callee cleans the stack, which is impossible for vararg functions.

Arguments are pushed on the stack from right to left,

with the this pointer being passed via register ECX,

and not on the stack, on the x86 architecture.


内容的には一緒っぽい。


Google先生に「mingw thiscall calling convention」と聞いてみた*4

Mingw switched abis with the release of gcc 4.7 (http://gcc.gnu.org/gcc-4.7/changes.html).

The main change is that now mingw (like msvc) given thiscall calling convention to methods by default.


ということで、MinGWはとあるバージョンからthiscallに変更されたようだ。

ちなみにリンク先*5を見ても

IA-32/x86-64

Windows x86 targets are using the __thiscall calling convention for C++ class-member functions.

とシンプルに書かれているだけで読み取れない・・・

で、ここまで調べてふと「そもそも呼び出し側で引数をpushしていない」ことに気が付いた。

呼び出し側は関数の先頭でドカンとスタックを取ってからmovで個別にデータを入れている。pushしていないのだから、呼び出し先でスタック調整したら呼び出し元でも再調整しないと一致しない。だからthiscall対応済みのMinGWには呼び出し元に"再調整の"コードが入っていた、と。


試したこと

clangのドキュメント*6によるとthiscallというattributeがあるらしい。

ちょうどアプリビルド時にdllimportで警告になっていたのでdefineでdllimportをthiscallに変更してみた。

が、dllimportはclassに対してつけられているようだけど、clangはメソッドにだけ対応しているらしい。

個別に各メソッドに付けないと効果を発揮しないようだ。

というところでギブアップ。


試していないこと

Qtソースコード改造

デメリットとして、

  • あちこち修正ないとダメ
  • 手を入れた時点でソース開示とか色々ライセンス上の制約を考慮することになる

Qt本体をclangでビルドし直す

世界のどこかには試した人がいるらしい。成功したのかどうか不明。

詳細なやり方は未調査。


余談

正直そこまでしてclang使います?ということで本件は保留中。

MinGWで問題なく使えそうだし。

どうしてもclang使いたい時はC言語でDLL作って呼び出せば良いしね。


あ、そういえばclangの3.7はWindows版バイナリが配布されてるっぽいね。

64ビット版もあるし入れようかなどうしようかな。

その前にMinGWの64ビット版を入れないとダメか。

2015-05-17

boost::gil+libpngでタイル画像への結合ツール作ってみた

| 17:50

ということで、早速boost::gilを試してみた。

手元のboost v1.55.0 には普通に含まれているようだ。

makefile

ここは特筆すべきこと無し。

CC		= clang
LIB_TOP	= C:\maminus\lib
BO_TOP	= $(LIB_TOP)\boost_1_55_0
INC		=							\
	-I $(BO_TOP)							\
	-I .								\
	-I $(LIB_TOP)\libpng						\
	-I $(LIB_TOP)\zlib\include					\

LIBS	=								\
	$(LIB_TOP)\libpng\libpng16-0.dll				\
	$(LIB_TOP)\zlib\zlib1.dll					\
	-lstdc++							\

CFLAGS	= -Wall -W -std=c++11 -Wno-c++11-narrowing
LFLAGS	= -static
SRCS	= 					\
	main.cpp				\

OBJS	= $(SRCS:.cpp=.o)
TARGET	= tiled-png.exe
RM		= del /f /q


.PHONY: all debug clean delete_objs delete_target

all: CFLAGS += -O2
all: $(TARGET)

debug: CFLAGS += -g
debug: LFLAGS += -g
debug: $(TARGET)

$(TARGET): $(OBJS)
	$(CC) -o $@ $(OBJS) $(LFLAGS) $(LIBS)

clean: delete_objs delete_target

delete_objs:
	$(RM) $(OBJS)

delete_target:
	$(RM) $(TARGET)


.SUFFIXES: .cpp .o

.cpp.o:
	$(CC) -c $(INC) $(CFLAGS) -o $*.o $<

ツールのソース

ここもコメントに書いた通りなので書くことが無い。

注意点はint_p_NULLの定義がlibpng側から消滅してるって箇所くらい。

boost::gilの使い方も参考URL側がとてもわかりやすい。

//-----------------------------------------------------------------------------
#include <boost/format.hpp>
#include <boost/lexical_cast.hpp>
namespace
{		// コマンドライン引数系
const int	INVALID_ARG_VAL	= -1;
int		column_count	= INVALID_ARG_VAL;	// タイル画像の列数
int		row_count	= INVALID_ARG_VAL;	// 行数
int		left		= INVALID_ARG_VAL;	// 切り出し矩形の左上 x 座標
int		top		= INVALID_ARG_VAL;	// 同 y 座標
int		right		= INVALID_ARG_VAL;	// 同左下の x 座標
int		bottom		= INVALID_ARG_VAL;	// 同 y 座標

// コマンドライン引数をグローバル変数へ展開する
//	戻り値:
//		0    :正常
//		0以外:異常
int parse_args(int argc, char *argv[])
{
	if(argc < 7){
		return -1;
	}

	int			i;
	try{
		int		*arg_tbl[] = {NULL, &column_count, &row_count, &left, &top, &right, &bottom};
		for(i=1;i<=6;i++){		// 前から順番に整数として読み出す
			*arg_tbl[i] = boost::lexical_cast<int>(argv[i]);
		}
	}catch(const std::exception &e){	// 読み出せない形式ならここを通る
		std::cerr << str(boost::format("argv[%d]: %s is not int? -> %s") % i % argv[i] % e.what()) << std::endl;
		return -2;
	}

	// 引数の整合性チェック
	if((left < 0) || (left >= right)){
		std::cerr << str(boost::format("left must >= 0 and < right. but left:%d right:%d") % left % right) << std::endl;
		return -4;
	}
	if((top  < 0) || (top >= bottom)){
		std::cerr << str(boost::format("top must >= 0 and < bottom. but top:%d bottom:%d") % top % bottom) << std::endl;
		return -4;
	}

	// ファイル数のチェック
	int		file_count = argc - 7;
	if((file_count == 0) || (file_count % (column_count * row_count) != 0)){
		std::cerr << str(boost::format("file list count %d is not a multiple of %d (%d x %d)") % file_count % (column_count * row_count) % column_count % row_count) << std::endl;
		return -3;
	}

	return 0;
}
void usage(const char *argv0)
{
	std::cout << str(boost::format("Usage:\n\t>%s column_count row_count left top right bottom files...") % argv0) << std::endl;
}
}
//-----------------------------------------------------------------------------

//-----------------------------------------------------------------------------
//http://stackoverflow.com/questions/2442335/libpng-boostgil-png-infopp-null-not-found
//	ここによるとlibpng 1.4以降以下の定義がなくなってしまったとのこと
//	コンパイルエラーになるので自前で定義しておく
#define png_infopp_NULL (png_infopp)NULL
#define int_p_NULL (int*)NULL
#include <boost/gil/gil_all.hpp>
#include <boost/gil/extension/io/png_io.hpp>
namespace
{		// PNGファイル処理系

//タイル画像を生成する
//	参考URL: http://d.hatena.ne.jp/tsurushuu/20080723/1216783641
void generate_tiled_image(int number, char *files[])
{
	int		width	= right - left;
	int		height	= bottom - top;

	// 出力先を用意する (横幅×列数、縦幅×行数)
	boost::gil::rgba8_image_t	dst(width * column_count, height * row_count);

	for(int y=0;y<row_count;y++){			// 縦方向のループ(上から下に向けて画像を結合していく)
		for(int x=0;x<column_count;x++){	// 横方向のループ(左から右に向けて画像を結合していく)
			// 元画像をロードする
			boost::gil::rgba8_image_t src;
			boost::gil::png_read_image(files[y*column_count + x], src);

			// 元画像から指定範囲を切り出して出力先にコピーする
			boost::gil::copy_pixels(
				boost::gil::subimage_view(boost::gil::view(src), left,    top,      width, height),
				boost::gil::subimage_view(boost::gil::view(dst), x*width, y*height, width, height)
				);
		}
	}

	// 0.png, 1.png, 2.png, ... の名前で画像をファイルに保存する
	boost::gil::png_write_view(str(boost::format("%d.png") % number), boost::gil::view(dst));
}
}
//-----------------------------------------------------------------------------

//-----------------------------------------------------------------------------
int main(int argc, char *argv[])
{
	if(parse_args(argc, argv) != 0){	// コマンドライン引数をグローバル変数へ展開する
		usage(argv[0]);			// 引数がおかしい場合はUsage表示して終了する
		return -1;
	}

	int j = 0;
	for(int i=7;i<argc;i+=(column_count * row_count)){		// ファイル1セット分(列数×行数)ずつ
		try{
			generate_tiled_image(j++, argv + i);		// タイル画像に変換する
		}catch(const std::exception &e){
			std::cerr << e.what() << std::endl;
		}
	}

	return 0;
}
//-----------------------------------------------------------------------------

参考URL:

http://d.hatena.ne.jp/tsurushuu/20080723/1216783641

http://stackoverflow.com/questions/2442335/libpng-boostgil-png-infopp-null-not-found


今回はRGBAのpng画像を使うのでrgba8_image_tを使用したが、RGBのpngだとrgb8_image_tになる。

というか、最初rgb8_image_tを使っていたら以下のようなエラーになってしまった。

png_read_view: input view type is incompatible with the image type

boost::gil側をgrepしたら画像の種類が一致してないときに出るエラーだったので、ソースと画像側が一致してないとダメらしい。


ツール実行

で、このツールは以下のように呼び出す。

C:\maminus>tiled-png.exe 2 3 20 20 30 40 src1.png src2.png src3.png src4.png src5.png src6.png

動作イメージはこんな感じ。例だと6つの画像から矩形領域を切り取って2x3のタイル画像にしている。矩形領域がちょうど番号部分になっているので結果の画像は番号だけの画像になっている。

f:id:maminus:20150516204723p:image

ツールのコマンドライン引数はこんな感じ。タイルの列数、行数、矩形領域のピクセル位置を指定する。

f:id:maminus:20150516204724p:image

VBScript

で、毎回切り出し範囲などを指定するのは面倒なのでスクリプト化してみた。

ツールのexeとこのスクリプトは同じフォルダに入れる。

そして、このスクリプトファイルへのショートカットを「送る」に登録すれば画像ファイルを「送る」だけになるはず。

(毎回同じ範囲の画像を結合する前提になるけど)

' 引数は画像ファイルだけ指定してもらう
' (ショートカットを「送る」に入れてもらうような用途を想定)

Option Explicit

Dim objPrm
Set objPrm = Wscript.Arguments		' コマンドライン引数

' 引数チェック
If objPrm.Count Mod 6 <> 0 Then
	MsgBox "ファイルを6個ずつ指定してください。"
	WScript.Quit
End If


Dim objFs
Set objFs = CreateObject("Scripting.FileSystemObject")
Dim objShell
Set objShell = WScript.CreateObject("WScript.Shell")

' スクリプトのフォルダをカレントディレクトリに設定する
'	http://d.hatena.ne.jp/ku__ra__ge/20111110/p2
objShell.CurrentDirectory = objFs.GetFile(WScript.ScriptFullName).ParentFolder.Path


Const WindowHide = 0	' ウィンドウを非表示
Dim i
Dim target				' 出力ファイル名
Dim command				' コマンド実行用

' コマンドライン文字列を組み立てていく
' 固定パラメータでツールを実行する
' パラメータを頻繁に変更する場合は Const 変数とかに分離したほうが楽そう
command = "tiled-png.exe 2 3 330 95 790 465"
For i=0 to objPrm.Count / 6 - 1
	command = command&" "& objPrm(i*6+0)&" "& objPrm(i*6+1)&" "& objPrm(i*6+2)&" "& objPrm(i*6+3)&" "& objPrm(i*6+4)&" "& objPrm(i*6+5)
Next

' コマンド実行する
objShell.Run command, WindowHide

For i=0 to objPrm.Count / 6 - 1
	' 入力ウィンドウを表示してユーザさんに正式ファイル名を入力してもらう
	' デフォルト名にそれっぽい初期値を入れておく
	target = InputBox("ファイル名を入れてください", "正式ファイル名入力", "xxx_第"&(i+1)&"画像.png")

	' ファイル名を変更する
	objFs.MoveFile (i)&".png", target
Next

Set objShell = Nothing
Set objFs = Nothing
Set objPrm = Nothing

参考URL:WSHで、強制的にカレントディレクトリを設定する方法 - くらげのChangeLog


これで画像ファイルを作れば後はまとめて「送る」だけで結合ファイルができるようになる。

ただ、1点大問題がある。

「送る」でたくさんのファイルを指定すると、どうもファイルの順番がぐちゃぐちゃになるように見える。

VB側でソートするのは面倒なのでC++側でソートするくらいしかないかなと思う。

結合前のファイルは名前順になるように例えば連番とかにしておけば良いかと。


余談

と、ここまで書いて「でもこの程度ならスクリプトに対応したフォトレタッチソフト使えば同じことできるよね」と思った。

例えばGIMPってスクリプトに対応していたような気がする。

まぁ、「送る」で処理できるか、とか処理速度というかソフトの起動時間はどうか、とかいろいろあるかもしれないけど今回やったことも結局は車輪の再発明くさいねぇ。

とりあえず今回はC++(clang with Windows)から簡単にPNGを読み書きできる手段を手に入れたってことで。

今まではずっとBMPファイルでやってきたから個人的にうれしい進歩だね。

出力はサイズを気にしなければBMPファイルでも良いけど、特に入力ファイルに制約が出るのは困るしね。

いや、JPEGの導入方法調べてないじゃん、って言われると困るけど・・・

boost::gil自体はJPEG対応してるっぽいけど、libjpegが必要っぽい。とはいえ、バイナリが配布されている気配*1がするのでzlib同様DLLとヘッダファイルを展開してmakefileでパス設定したら終わりのような気もする)

2015-05-16

Win環境でlibpngのビルド

| 20:37

ふとpng画像をいじくり倒したくなってみたのだが、いつの間にやらboost::gilというのでlibpngが使えるようになっているらしい。

ただ、libpngは別途インストールが必要ということでやってみた。

zlib

libpngはzlibに依存しているらしいので、zlibをダウンロードする。

zlib.netに普通にバイナリが置いてあるのでそのままダウンロードして適当にzip解凍するだけで終了した。バイナリが置いてあると助かるねぇ。

libpng

libpngは公式サイトにバイナリが置いてなかった。大人の事情かな?

とりあえずsourceforgeからソースコードをダウンロード

ファイルを解凍してみたけど、例によってVC用のmakefileとかが入ってるけどさすがにclang用のファイルはなさそうに見える。ま、無いなら作れば良いよね。(廃人的思考)

以下自分が試した手順。

clangは v3.2

MinGwは v4.6.2

どこをどうすべきなのかは

  • INSTALL
  • README
  • scripts\pnglibconf.dfa

あたりとにらめっこして、後は感触とコンパイルエラー、ソースのgrepから推測で。

makefile

scripts\makefile.gccをベースにそれっぽくねつ造する。

以下コメントアウト箇所の周辺が変更箇所

# makefile for libpng using gcc (generic, static library)
# Copyright (C) 2008, 2014 Glenn Randers-Pehrson
# Copyright (C) 2000 Cosmin Truta
# Copyright (C) 1995 Guy Eric Schalnat, Group 42, Inc.
#
# This code is released under the libpng license.
# For conditions of distribution and use, see the disclaimer
# and license in png.h

# Location of the zlib library and include files
#ZLIBINC = ../zlib #フォルダ構成が違うので変更
ZLIBINC = ../zlib/include
ZLIBLIB = ../zlib

# Compiler, linker, lib and other tools
#CC = gcc #自分はclang使いなので変更(どんな理由だ)
CC = clang
LD = $(CC)
AR_RC = ar rcs
RANLIB = ranlib
#CP = cp #cpコマンドは無いので削除(処理的にも不要なので)
#RM_F = rm -f #rmコマンドは処理的に必要なので del コマンドに置き換え
RM_F = del /f /q

WARNMORE = -Wwrite-strings -Wpointer-arith -Wshadow \
	-Wmissing-declarations -Wtraditional -Wcast-align \
	-Wstrict-prototypes -Wmissing-prototypes # -Wconversion
CPPFLAGS = -I$(ZLIBINC) # -DPNG_DEBUG=5
#CFLAGS = -W -Wall -O2 # $(WARNMORE) -g #DLLビルド用のオプションに変更
CFLAGS =  -W -Wall -O2 -fPIC -fvisibility=hidden
LDFLAGS =
# DLLビルド用オプションを追加
LFLAGS  = -shared -static -Wl,--add-stdcall-alias
#LIBS = -lz -lm # テストプログラムの使用ライブラリがこちらの環境と異なるので変更
LIBS = $(ZLIBLIB)/zlib1.dll

# File extensions
#EXEEXT = # Windowsなので拡張子変更
EXEEXT = .exe

# 下の方のpnglibconf.hルール削除に伴い誰も使わない定義を削除
# Pre-built configuration
# See scripts/pnglibconf.mak for more options
#PNGLIBCONF_H_PREBUILT = scripts/pnglibconf.h.prebuilt

# Variables
OBJS =  png.o pngerror.o pngget.o pngmem.o pngpread.o \
	pngread.o pngrio.o pngrtran.o pngrutil.o pngset.o \
	pngtrans.o pngwio.o pngwrite.o pngwtran.o pngwutil.o

# Targets
#all: static # DLLビルド用のルール追加
all: static shared

# ここはまるごと削除。当該.hは自前でコピー&改変済み
#pnglibconf.h: $(PNGLIBCONF_H_PREBUILT)
#	$(CP) $(PNGLIBCONF_H_PREBUILT) $@

.c.o:
	$(CC) -c $(CPPFLAGS) $(CFLAGS) -o $@ $<

static: libpng.a pngtest$(EXEEXT)

# こちらの環境に合わせたDLLビルドルールに変更
#shared:
#	@echo This is a generic makefile that cannot create shared libraries.
#	@echo Please use a configuration that is specific to your platform.
#	@false
shared: $(OBJS)
	$(CC) -o libpng16-0.dll $(OBJS) $(LFLAGS) $(LIBS)

libpng.a: $(OBJS)
	$(AR_RC) $@ $(OBJS)
	$(RANLIB) $@

test: pngtest$(EXEEXT)
	./pngtest$(EXEEXT)
	copy /b $(LIBS) .
#	↑を追記。zlibのdllが無いとテストプログラムが実行できない

pngtest$(EXEEXT): pngtest.o libpng.a
	$(LD) $(LDFLAGS) -L$(ZLIBLIB) -o $@ pngtest.o libpng.a $(LIBS)

clean:
	$(RM_F) *.o libpng.a pngtest$(EXEEXT) pngout.png
#	$(RM_F) *.o libpng.a pngtest$(EXEEXT) pngout.png pnglibconf.h # .hは自前で持ってきてるので削除されると困るので除外

png.o:      png.h pngconf.h pnglibconf.h pngpriv.h pngstruct.h pnginfo.h pngdebug.h
pngerror.o: png.h pngconf.h pnglibconf.h pngpriv.h pngstruct.h pnginfo.h pngdebug.h
pngget.o:   png.h pngconf.h pnglibconf.h pngpriv.h pngstruct.h pnginfo.h pngdebug.h
pngmem.o:   png.h pngconf.h pnglibconf.h pngpriv.h pngstruct.h pnginfo.h pngdebug.h
pngpread.o: png.h pngconf.h pnglibconf.h pngpriv.h pngstruct.h pnginfo.h pngdebug.h
pngread.o:  png.h pngconf.h pnglibconf.h pngpriv.h pngstruct.h pnginfo.h pngdebug.h
pngrio.o:   png.h pngconf.h pnglibconf.h pngpriv.h pngstruct.h pnginfo.h pngdebug.h
pngrtran.o: png.h pngconf.h pnglibconf.h pngpriv.h pngstruct.h pnginfo.h pngdebug.h
pngrutil.o: png.h pngconf.h pnglibconf.h pngpriv.h pngstruct.h pnginfo.h pngdebug.h
pngset.o:   png.h pngconf.h pnglibconf.h pngpriv.h pngstruct.h pnginfo.h pngdebug.h
pngtrans.o: png.h pngconf.h pnglibconf.h pngpriv.h pngstruct.h pnginfo.h pngdebug.h
pngwio.o:   png.h pngconf.h pnglibconf.h pngpriv.h pngstruct.h pnginfo.h pngdebug.h
pngwrite.o: png.h pngconf.h pnglibconf.h pngpriv.h pngstruct.h pnginfo.h pngdebug.h
pngwtran.o: png.h pngconf.h pnglibconf.h pngpriv.h pngstruct.h pnginfo.h pngdebug.h
pngwutil.o: png.h pngconf.h pnglibconf.h pngpriv.h pngstruct.h pnginfo.h pngdebug.h

pngtest.o:  png.h pngconf.h pnglibconf.h

pnglibconf.h

scripts\pnglibconf.h.prebuiltをpnglibconf.hに改名して以下を変更

  • PNG_BENIGN_WRITE_ERRORS_SUPPORTEDをdefine(不要かも)
  • PNG_API_RULEを1に変更
  • PNG_ZLIB_VERNUMを0x1280に変更
  • PNG_QUANTIZE_BLUE_BITS/GREEN_BITS/RED_BITSを8に変更(不要かも)
  • #define PNG_USER_PRIVATEBUILD "Build by personal for build clang's libpng binary."を追加

少し補足しておく。

PNG_ERROR_NUMBERS_SUPPORTEDはdefineするとpngerror.c L:46〜L:47のif文がかっこの対応がおかしいためコンパイルエラーとなる。非対応かもしれない。

PNG_BENIGN_WRITE_ERRORS_SUPPORTED側はコンパイルが通ったので入れてあるが、こちらも実は非対応かもしれない。

書き込みに失敗した時にエラーになるらしいけど。


PNG_ZLIB_VERNUMは zlib v1.2.8 をダウンロードしたので0x1280にした。

PNG_QUANTIZE_xxx_BITSはデフォルト5らしいけど、なんか心配なので8にしてみた。

どっちが正解なのか不明。

PNG_USER_PRIVATEBUILDはdefineしておかないと#error文でコンパイルエラーになっちゃう。

中身は多分何でも良いと思われるが、コンパイルし直した理由を記載するdefineの模様。


ビルド

作ったmakefileとpnglibconf.hはlibpngを解凍したトップフォルダに入れる。

.cや.h類もトップフォルダに入っているので、ごちゃっとするけど気にしない。

入れたらおもむろにトップフォルダでmakeするだけ。

C:\maminus\lib\libpng>mingw32-make

思ったよりは楽だった。(廃人的思考)