[software] TCHARはcharかwchar_tか

よく知られているように、Windows CE環境はAPIUnicodeしかサポートしていない。そのため、eVCで開発をする際、普通は_UNICODEが定義された状態であり、TCHARはwchar_tである。
また、Windows XP環境はANSIUnicodeもサポートしており、デスクトップ上のWindows環境でVisual Studioで開発をする際は普通は_MBCSが定義された状態である。この場合、TCHARはcharである。
気をつけていればあまり問題はおきないのだが、よく考えずにWindows CE上のものをWindows XPへ移植しようとするとはまることがある。筆者の経験した例を紹介しよう。
そのプロジェクトはお客様の環境(Windows CEの組込み機器)にとあるミドルウェアを納めるというものだったのだが、フォントの部分だけお客様(の発注した別のベンダーであろう)がDLLで用意する形であった。文字列に対応するフォントのビットマップデータを返すようなAPIをもっているDLLであったので、当然のように呼び出し関数の引数にはTCHAR *型のものがあった(const TCHAR *型でないところがまたやっかいだったのだが、この際置いておく)。ターゲット環境はWindows CEだったので、TCHARはwchar_tであり、要するにUnicode文字列からフォントデータを返す形である。
さて、実機の開発が終了した後、そのミドルウェアの上に載せるアプリケーション開発用に、Windows XP上で動作する同等のものが欲しいということになった。そこで、フォントのDLLもお客様よりWindows XP用のものを提供してもらったのだが、ここでハタと疑問がよぎる。APIに渡す文字列はマルチバイトなのだろうか、それともUnicodeなのだろうか?
お客様より提供されたWindows XP用のヘッダを見てみると、件のAPIのプロトタイプ宣言では引数の型がやはりTCHAR *になっている。これはWindows XPだからchar *でいいんだよな、と安易な解釈をしてミドルウェアをビルドしたところ、見事に動かない(正確にはC++だったので、引数の違いでリンカエラーがでたような。。ずいぶん前のことなので記憶があいまいだ)。どうやらこのDLLは実機に忠実にUnicode文字列を引数としてとるようになっているようだ。ミドルウェアの別の部分ではTCHARがcharであるとしてコーディングされている部分があったので、_UNICODEを定義するわけにもいかない。というか、ANSI版とUnicode版があるわけではないのだからTCHARをプロトタイプ宣言に使うのがそもそもの問題なのである。。結局お客様より提供されたヘッダ中のTCHARを全部wchar_tに書き換えることによって対応した。非常に無駄な作業である。

  • 教訓: マルチバイト版とUnicode版と両方をサポートするのでないのなら、DLLでexportするAPIのプロトタイプ宣言でTCHARを使うことは避けるべき。

[software]dlsym、GetProcAddressとname mangling

上でCとC++間での関数呼び出しの際にname manglingが壁になることを述べたが、C++で閉じている環境ではそんなことを普通は気にしなくていい。が、マイクロソフトの言う「明示的リンク]」というやつを使う場合は別だ。「明示的リンク」とはdlsymやGetProcAddressなどのAPIでDLLから目的の関数ポインタを得る方法でのDLLの使用のことを言うようである。
eVCでWindows CE向けの開発をしていると、C++が標準という気分になってしまう。筆者がこれではまったのもWindows CE向けプロジェクトだった。お客様から提供された、とある機能のDLLを使用することになったのだが、当初「暗黙的リンク」でリンクして納品したところ、そのDLLがない場合は別の方法を使用するようにして欲しい(つまり、そのDLLがなくても動作するように作って欲しい)と要求され、「明示的リンク」でリンクすることにした。そこで、やってみてはたと気がついたのだが、GetProcAddressに関数名を普通に渡しても失敗するのである。どうやらお客様(正確にはお客様が作業依頼している別のベンダだと思われるが)が、件のDLLをC++で作成し、シンボルがname manglingされているようだ。「暗黙的リンク」でリンクした場合にはこちらもC++コンパイラを使っていたから特にリンカエラーも出ずに気がつかなかった。そう、dlsymやGetProcAddressなどの「明示的リンク」で呼び出されることを考慮するならば、呼び出し側も含めてC++で閉じていようとも、extern "C"を指定して、name manglingしないようにするべきなのである。ちなみに、筆者が取るべき行動は、

  • 「お客様にDLLのシンボルのname manglingをやめてもらうようにお願いする」
  • 「dumpbinでexportされているシンボルをみて、GetProcAddressに渡すシンボル名を調べる」

のどちらかであった。正義感に燃えるエンジニアとしては前者を選びたいところだが、お客様を通してこのDLLを作成しているであろうベンダとやりとりするのが非常に面倒そうだったので、後者を採用してしまった。これも処世術である。

[software]extern "C"

overloadを可能にし、またリンク時の引数チェック(コンパイル時ではなく)も可能にするname manglingであるが、C++とCを混ぜて使う場合にはちょっとやっかいなことになる。何も捻らずまじめに考えるとCで記述されたコードからC++の関数を呼ぶためには、C側でname manglingされたシンボル参照をしないといけないことになる。また、C++で記述されたコードからC側の関数を呼ぶ手段がないように思えてしまう。このような問題を解決するにはC++コンパイラにname manglingさせなければよい。そのために、extern "C"指定子を使う。このことはよく知られていると思う。


$ cat hoge.cpp
extern "C" int f(int i)
{
return i + 1;

}
$ g++ hoge.cpp -c -o hoge.o
$ nm hoge.o
00000000 b .bss
00000000 d .data
00000000 t .text
00000000 T _f <----name manglingされていない

上の例はC++で記述された関数のシンボルが、Cから呼べるようにname manglingされていない例である。逆に、C++から呼ばれる可能性のある、Cで記述されたライブラリなどは必ずヘッダファイル中でexportする関数のプロトタイプ宣言を

#ifdef __cplusplus
extern "C" {
#endif

void *memset (void *, int, size_t);
char *strcat (char *, const char *);
int strcmp (const char *, const char *);
char *strcpy (char *, const char *);

#ifdef __cplusplus
}
#endif
のように囲む(この例はWikipediaより)。こうすればC++コンパイラは関数への参照をname manglingしない形で行うので、無事にC++からCの関数を呼ぶことができる。

[software]C++でDLL (Windows CE)

過去経験したことで、いくつかC++Windows CE向けのDLLを作成するに当たってはまったことがあったので、何日かに分けてそれを書いておこうと思う。
まずは、Windows CEに限った話ではないのだけど、C++ということでname manglingについてのお話。一般的に、C++コンパイラでビルドされたバイナリのシンボルは関数の引数種などを反映させる(これによりoverloadを可能にする)などのために、ソースコード中に書いた名前を変形させたものになっている。たとえば以下のように。


$ cat hoge.cpp
int f(int i)
{
return i + 1;

}
$ g++ hoge.cpp -c -o hoge.o
$ nm hoge.o
00000000 b .bss
00000000 d .data
00000000 t .text
00000000 T __Z1fi <----これがf(int i)のなれのはて

残念なことに、どのようにname manglingを行うかについてはC++で規定されているわけではなく、コンパイラ依存である。Windows CEC++を使った開発の経験がある向きはリンカエラーのメッセージで関数名の後ろにやたらいっぱい大文字のアルファベットがくっついている名前が表示されるのを見たことがあるだろう。

automake & autoconf 続き

さて、例として簡単なQt/EmbeddedをリンクするDLLを作ってみることにする。組込み向けっぽくクロスコンパイルする。

ソース類はこんな感じ。


$ cat src/hoge.cpp
#include
#include "fuga.h"

QPushButton *
hoge()
{
QPushButton *button = new QPushButton("hoge", 0);

return button;
}

$ cat src/foo.cpp
#include
#include "fuga.h"

QPushButton *
foo()
{
QPushButton *button = new QPushButton("foo", 0);

return button;
}

$ cat src/include/fuga.h
#ifndef __FUGA_H__
#define __FUGA_H__

//dummy
#define LIBNAME FOOHOGE
#ifdef __cplusplus
extern "C" {
#endif

extern QPushButton *foo();
extern QPushButton *hoge();

#ifdef __cplusplus
}
#endif

#endif

とても適当な感じだが、ようするに関数fooとhogeをexportするということである。

初期状態でのディレクトリ構成はこのように。


$ find
.
./src
./src/foo.cpp
./src/include
./src/include/fuga.h
./src/hoge.cpp

上のツリーにはMakefileの雛形が全く含まれていない。当然必要となるので、Makefile.am、src/Makefile.amを書く


$ cat src/Makefile.am
lib_LTLIBRARIES = libhogefoo.la
libhogefoo_la_SOURCES = foo.cpp hoge.cpp
libhogefoo_la_CPPFLAGS = -Iinclude
最終的にlibhogefoo.soを作りたいのである。

$ cat Makefile.am
SUBDIRS = src
トップディレクトリにはとりあえずこれだけを書いておく。

Makefile.amの準備ができたらautoscanする。


$ autoscan
autom4te: configure.ac: no such file or directory
autoscan: /usr/bin/autom4te failed with exit status: 1
$ ls
autoscan.log configure.scan src
autoscanすると、configure.scanができているのでconfigure.acの雛形として使う。failしているようだがとりあえず気にしない。
configure.acに書き足す内容としては、

  • automakeを使うので、AM_INIT_AUTOMAKEが、
  • libtoolを使うので、AC_PROG_LIBTOOLが

それぞれ必要である。


$ cat configure.ac
# -*- Autoconf -*-
# Process this file with autoconf to produce a configure script.

AC_PREREQ(2.59)
AC_INIT(libfoo, 0.1, no-define)
AC_CONFIG_SRCDIR([src/foo.c])
AM_INIT_AUTOMAKE([-Wall -Werror foreign])
AC_CONFIG_HEADER([config.h])

# Checks for programs.
AC_PROG_CC
AC_PROG_LIBTOOL

# Checks for libraries.

# Checks for header files.

# Checks for typedefs, structures, and compiler characteristics.

# Checks for library functions.

AC_CONFIG_FILES([Makefile
src/Makefile])
AC_OUTPUT

autoreconfでconfigureをgenerate!

configure.acを書き終えたら、autoheader、aclocal、libtoolize、automake、autoconfをそれぞれ走らせるわけ
だが、最近のautotoolsではautoreconfでまとめてやってくれるようだ。


$ autoreconf --install
configure.ac: installing `./install-sh'
configure.ac: installing `./missing'
src/Makefile.am: installing `./compile'
src/Makefile.am: installing `./depcomp'
$ ls
Makefile.am Makefile.in aclocal.m4 autom4te.cache autoscan.log compile
config.guess config.h.in config.sub configure configure.ac
configure.scan depcomp install-sh ltmain.sh missing src
これで一応の体裁は整った。しかし、当然ながらこれだとQt/Embeddedを探しに行ってくれない。ということで以下をconfigure.ac
に追加する。

# Checks for libraries.
AC_ARG_WITH([qte],
[AS_HELP_STRING([--with-qte=/path/to/qte], [point to location where Qte exists])],
[LDFLAGS=-L$withval/lib
CFLAGS="$CFLAGS -I$withval/include -DQT -DQWS -fno-rtti -D_GNU_SOURCE"
CXXFLAGS="$CXXFLAGS -I$withval/include -DQT -DQWS -fno-rtti -D_GNU_SOURCE"
AC_CHECK_LIB([qte], [qt_get_screen], [],
[AC_MSG_ERROR([Qte was not found!])])

],
[AC_MSG_ERROR([Qte is required!])])

AC_ARG_WITH([qtthreaded],
[AS_HELP_STRING([--with-qtthreaded], [Using thread functions of Qt])],
[CFLAGS="$CFLAGS -DQT_THREAD_SUPPORT"
CXXFLAGS="$CXXFLAGS -DQT_THREAD_SUPPORT"],
[])

--with-qte=[Qteの場所]で、Qt/Embeddedのインストールされている場所を指定することにする。
--with-qtthreadedでQteのスレッドサポートの可否も選べるようにしておく。
また、g++を使うのと、libtoolがstatic libraryも作るためにranlibとarを使うことから以下も追加。前述のように今回はクロスコンパイルなので、configure時にCC、CXXなどの環境変数を適宜設定する必要がある。

AC_PROG_CXX
AC_PROG_RANLIB
AC_CHECK_TOOL([AR], [ar], [:])
完成したconfigure.acはこのようになった。

$ cat configure.ac
# -*- Autoconf -*-
# Process this file with autoconf to produce a configure script.

AC_PREREQ(2.59)
AC_INIT(libfoo, 0.1, no-define)
AC_CONFIG_SRCDIR([src/foo.cpp])
AM_INIT_AUTOMAKE([-Wall -Werror foreign])
AC_CONFIG_HEADER([config.h])

# Checks for programs.
AC_PROG_CC
AC_PROG_CXX
AC_PROG_RANLIB
AC_CHECK_TOOL([AR], [ar], [:])
AC_PROG_LIBTOOL

# Checks for libraries.
AC_ARG_WITH([qte],
[AS_HELP_STRING([--with-qte=/path/to/qte], [point to location where Qte exists])],
[LDFLAGS=-L$withval/lib
CFLAGS="$CFLAGS -I$withval/include -DQT -DQWS -fno-rtti -D_GNU_SOURCE"
CXXFLAGS="$CXXFLAGS -I$withval/include -DQT -DQWS -fno-rtti -D_GNU_SOURCE"
MOC_CMD=$withval/bin/moc
AC_CHECK_LIB([qte], [qt_get_screen], [],
[AC_MSG_ERROR([Qte was not found!])])

],
[AC_MSG_ERROR([Qte is required!])])
AC_SUBST([MOC_CMD])

AC_ARG_WITH([qtthreaded],
[AS_HELP_STRING([--with-qtthreaded], [Using thread functions of Qt])],
[CFLAGS="$CFLAGS -DQT_THREAD_SUPPORT"
CXXFLAGS="$CXXFLAGS -DQT_THREAD_SUPPORT"],
[])


# Checks for header files.

# Checks for typedefs, structures, and compiler characteristics.

# Checks for library functions.

AC_CONFIG_FILES([Makefile
src/Makefile])
AC_OUTPUT

configure.acに変更を加えた際には、autoreconfすると変更が反映され、configureが新しくなる。ビルドするにはまずconfigureスクリプトを実行するわけだが、今回はターゲットがpower pc linuxである。コンパイラにつくprefixはppc_405-である。(ppc_405-gccppc_405-g++など)
./configureにはデフォルトでクロスコンパイルターゲットを指定する--hostオプションがあるが、config.subが知っているバリエーションでないとうまくいかない。
結局今回は

CC=ppc_405-gcc CXX=ppc_405-g++ RANLIB=ppc_405-ranlib AR=ppc_405-ar ./configure --host ppc-linux --build i586-pc-linux-gnu --with-qte=/opt/qt-2.3.2
とした。

automake & autoconf & libtoolではまった/はまりそうな点

AC_FUNC_MALLOC

ソース中にmallocがある場合、autoscanの結果、configure.acにはAC_FUNC_MALLOCが定義される。このAC_FUNC_MALLOCは、autoconfで展開された結果、mallocの挙動を調査するコードとなってconfigureに含まれる。
その調査の内容だが、malloc(0)がvalidなポインタを返すかどうかをチェックし、もしvalidなポインタを返さない場合には、mallocがrpl_mallocとしてdefineされてしまう。ついでにAC_LIBOBJにmallocが追加される。autoconfの意図としては「malloc.cにて、rpl_mallocという名前で正しい挙動をするmallocラッパーを実装せよ」ということなんだろうが、Makefileを書くのが面倒だからautotoolsを使っている身分としては大きなお世話である。ということで、そもそもmallocに0なんか渡らないと確信できる現実主義者はAC_FUNC_MALLOCをconfigure.acから削ってよし。

Conditional Sources for libraries

サウンド関係など、プラットフォームによって差異が大きいものは、#ifdefで分けるよりもファイルをプラットフォーム毎に用意した方がすっきりする。こういう場合、automakeのマニュアルにはEXTRA_hogehoge_SOURCESを使うといいよと書いてある。


8.1.3.1 Conditional compilation using _LDADD substitutions

Automake must know all the source files that could possibly go into a program, even if not all the files are built in every circumstance. Any files that are only conditionally built should be listed in the appropriate EXTRA_ variable. For instance, if hello-linux.c or hello-generic.c were conditionally included in hello, the Makefile.am would contain:

bin_PROGRAMS = hello
hello_SOURCES = hello-common.c
EXTRA_hello_SOURCES = hello-linux.c hello-generic.c
hello_LDADD = $(HELLO_SYSTEM)
hello_DEPENDENCIES = $(HELLO_SYSTEM)

You can then setup the `$(HELLO_SYSTEM)' substitution from configure.ac:

...
case $host in
*linux*) HELLO_SYSTEM='hello-linux.$(OBJEXT)' ;;
*) HELLO_SYSTEM='hello-generic.$(OBJEXT)' ;;
esac
AC_SUBST([HELLO_SYSTEM])
...

In this case, the variable HELLO_SYSTEM should be replaced by either hello-linux.o or hello-generic.o, and added to both hello_DEPENDENCIES and hello_LDADD in order to be built and linked in.

これをDLLのビルドに当てはめた場合は以下のように記述されている。

8.3.4 Libtool Libraries with Conditional Sources

Conditional compilation of sources in a library can be achieved in the same way as conditional compilation of sources in a program (see Conditional Sources). The only difference is that _LIBADD should be used instead of _LDADD and that it should mention libtool objects (.lo files).

So, to mimic the hello example from Conditional Sources, we could build a libhello.la library using either hello-linux.c or hello-generic.c with the following Makefile.am.

lib_LTLIBRARIES = libhello.la
libhello_la_SOURCES = hello-common.c
EXTRA_libhello_la_SOURCES = hello-linux.c hello-generic.c
libhello_la_LIBADD = $(HELLO_SYSTEM)
libhello_la_DEPENDENCIES = $(HELLO_SYSTEM)

And make sure configure defines HELLO_SYSTEM as either hello-linux.lo or hello-generic.lo.

Or we could simply use an Automake conditional as follows.

lib_LTLIBRARIES = libhello.la
libhello_la_SOURCES = hello-common.c
if LINUX
libhello_la_SOURCES += hello-linux.c
else
libhello_la_SOURCES += hello-generic.c
endif

さて、EXTRA_libhello_la_SOURCESの方法を使ったとしよう。ここで、hello-common.cもhello-linux.cもhello-generic.cもincludeディレクトリにあるhello.hをincludeする場合にはこのようにMakefile.amを書くことになる。

lib_LTLIBRARIES = libhello.la
libhello_la_SOURCES = hello-common.c
libhello_la_CPPFLAGS = -Iinclude
EXTRA_libhello_la_SOURCES = hello-linux.c hello-generic.c

libhello_la_LIBADD = $(HELLO_SYSTEM)
libhello_la_DEPENDENCIES = $(HELLO_SYSTEM)
configure.ac側は普通に考えると以下のようになるだろう。LIBTOOLを使うのでオブジェクトの拡張子は.loである。

...
case $host in
*linux*) HELLO_SYSTEM='hello-linux.lo' ;;
*) HELLO_SYSTEM='hello-generic.lo' ;;
esac
AC_SUBST([HELLO_SYSTEM])
...
これでやってみるとなぜかうまくいかない。hello-generic.cをコンパイルする際に-IincludeがCPPFLAGSから抜け落ちるのだ(筆者の環境はautomake 2.59)。生成されたMakefileをよーく眺めてみると、hello-generic.loではなくてlibhello_la-hello-generic.loに対するルールが書かれているではないか!

libhello_la-hello-generic.lo: hello-generic.c
if $(LIBTOOL) --tag=CC --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES)$(INCLUDES) $(libhello_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT libhello_la-hello-generic.lo -MD -MP -MF "$(DEPDIR)/libhello_la-hello-generic.Tpo" -c -o libhello_la-hello-generic.lo `test -f 'hello-generic.c' || echo '$(srcdir)/'`hello-generic.c; \
then mv -f "$(DEPDIR)/libhello_la-hello-generic.Tpo" "$(DEPDIR)/libhello_la-hello-generic.Plo"; else rm -f "$(DEPDIR)/libhello_la-hello-generic.Tpo"; exit 1; fi
と、いうことでconfigure.acでの正しい記述は以下のようになる。

...
case $host in
*linux*) HELLO_SYSTEM='libhello_la-hello-linux.lo' ;;
*) HELLO_SYSTEM='libhello_la-hello-generic.lo' ;;
esac
AC_SUBST([HELLO_SYSTEM])
...
しかしちょっとこれはいただけない(それ以前に非常に面倒だ)。たとえば、HELLO_SYSTEMの値を異なるDLLをビルドする二つのMakefile.amで参照しようとしたら破綻する。さらに、このようにlibhello_la-というprefixがつくという動作がどこまで規定されたものかがよくわからない。libtoolのマニュアルに書いてあるのかもしれないが。。面倒なことを気にしたくないのであれば、DLLのビルドの場合はAutomake conditionalsを使用するほうが吉であるようだ。

8.1.3.2 Conditional compilation using Automake conditionals

An often simpler way to compile source files conditionally is to use Automake conditionals. For instance, you could use this Makefile.am construct to build the same hello example:

bin_PROGRAMS = hello
if LINUX
hello_SOURCES = hello-linux.c hello-common.c
else
hello_SOURCES = hello-generic.c hello-common.c
endif

In this case, configure.ac should setup the LINUX conditional using AM_CONDITIONAL (see Conditionals).

automake & autoconf

automake & autoconfは言わずとしれたUNIXプラットフォームでのソフトウェア開発支援ツールだ。
ざっくり言うと、

autoconf

  • Makefile中の環境依存部分を半自動検出するconfigureスクリプトを生成するツール。
    • configureスクリプトMakefile.inをMakefileの雛形として使用する。Makefile.inは自分で書く必要がある。
    • configureスクリプトはconfigure.inからautoconfコマンドで生成される。configure.inは自分で書く必要がある。(autoscanで自動生成できるが)

automake

  • Makefile.inを半自動生成してくれる。Makefile.amがその雛形。
  • automakeを使う場合はconfigure.inの代わりにconfigure.acを使う。

また、automakeでDLLを作る場合にはlibtoolを必然的に使うことになる。詳しくはマニュアルを見ていただきたい。
広く使われているツールではあるが、筆者の業界である、組み込み&プロプライエタリ系ではあまり使われているのを見かけなかったりする。
信じられないかもしれないが、いちいちMakefileに書いてある
CC=arm-linux-gcc

CC=iwmmx_le-gcc
に書き換えたり、
QTDIR=/opt/hogehoge/qt-3.1

QTDIR=/opt/fugafuga/qt-2.3
に書き換えたりということをしばしば手で行うのだ。面倒なことこの上ない。
と、いうことでQt/Embeddedを使用した、とあるDLLのビルドプロジェクトをautomake対応にすることにした。