Tclのトークン分割機能をC/C++から直接呼び出してみるテスト

Tcl を C/C++ で活用する・・・というと、Tcl_Eval で Tcl の各種コマンドを呼び出す方法をまず思いつきますが、
Tcl_Eval を使う以外にも、文字列処理や、ファイル処理、参照カウンタ式のオブジェクト管理等、
色々便利そうな API が結構揃っていて、純粋に C/C++ 用のライブラリとしても使えそうな感じです。

さらに欲張って、Tcl がスクリプトを内部形式に変換する際に使用している
トークン分割処理を使えないか調べてみたのでその結果をちょっと書いてみます。

Tcl のトークン分割処理は、 Tcl_ParseCommand という C API から呼び出せるようです。
Tcl の文法に沿った文字列を試しに入れて、結果を表示してみます。

<実験コード>
void tokenize_test(){
  const char *some_tcl_script[] = {
    "hello {ABC DEF GHI} [hoge AA BB $CC] $def OKOK # comment start",
    "hello {ABC DEF \nGHI} [hoge AA BB $CC] $def OKOK #comment start"
  };
  for (int j = 0; j < 2; ++j){
    std::printf("[%d]\n", j+1);
    Tcl_Parse parse;
    ::Tcl_ParseCommand(0, some_tcl_script[j], -1, 0, &parse);
    std::printf("command:");
    std::fwrite(parse.commandStart, parse.commandSize, 1, stdout);
    std::printf("\n");
    for (int i = 0; i < parse.numTokens; ++i){
      Tcl_Token &tok = parse.tokenPtr[i];
      std::printf("token[%d] (%d):", i + 1, tok.type);
      std::fwrite(tok.start, tok.size, 1, stdout);
      std::printf("\n");
    }
    std::printf("comment:%s\n", parse.commentStart);
    ::Tcl_FreeParse(&parse);
  }
}

<結果>
[1]
command:hello {ABC DEF GHI} [hoge AA BB $CC] $def OKOK # comment start
token[1] (2):hello
token[2] (4):hello
token[3] (2):{ABC DEF GHI}
token[4] (4):ABC DEF GHI
token[5] (1):[hoge AA BB $CC]
token[6] (16):[hoge AA BB $CC]
token[7] (1):$def
token[8] (32):$def
token[9] (4):def
token[10] (2):OKOK
token[11] (4):OKOK
token[12] (2):#
token[13] (4):#
token[14] (2):comment
token[15] (4):comment
token[16] (2):start
token[17] (4):start
comment:(null)
[2]
command:hello {ABC DEF
GHI} [hoge AA BB $CC] $def OKOK #comment start
token[1] (2):hello
token[2] (4):hello
token[3] (2):{ABC DEF
GHI}
token[4] (4):ABC DEF
GHI
token[5] (1):[hoge AA BB $CC]
token[6] (16):[hoge AA BB $CC]
token[7] (1):$def
token[8] (32):$def
token[9] (4):def
token[10] (2):OKOK
token[11] (4):OKOK
token[12] (2):#comment
token[13] (4):#comment
token[14] (2):start
token[15] (4):start
comment:(null)
()内の数字は トークンの type を表していて、tcl.h に次のように定義されています。
#define TCL_TOKEN_WORD 1
#define TCL_TOKEN_SIMPLE_WORD 2
#define TCL_TOKEN_TEXT 4
#define TCL_TOKEN_BS 8
#define TCL_TOKEN_COMMAND 16
#define TCL_TOKEN_VARIABLE 32
#define TCL_TOKEN_SUB_EXPR 64
#define TCL_TOKEN_OPERATOR 128
#define TCL_TOKEN_EXPAND_WORD 256

スペースやタブをトークン区切りにして、各トークンを純粋に取り出したい、といった場合は、
TCL_TOKEN_WORD、または TCL_TOKEN_SIMPLE_WORD のトークンだけを処理して、それ以外の
トークンは無視すれば使えそうな感じでしょうか。

parse.commentStart がコメント開始箇所を見つけ出せてない理由が謎ですが・・・

Ghostscriptを使ってPDFにしおりをつける

フリーソフトでPDFにしおりを付加する機能のあるものがなかなか見つからなかった
のですが、実はGhostscriptを使えば結構簡単にできるということがわかりました。

しおりの文字列を日本語にしたい場合は、BOM付きのUTF16(BigEndian)を8進数表記にすれば
よいようなので、文字列=>UTF16 8進数表記変換をするついでにGhostscriptに渡すpdfmarkの
書式をある程度作るHTAツールを作ってみました。

<手順>
手順1. 下記内容を拡張子 .hta にして保存し、実行する。
手順2. 左側の欄にしおりの文字列を1項目1行で記入して"convert->"ボタンを押す。
手順3. 右側の欄の内容をテキストファイルに保存する。
手順4. テキストファイルの /Count や /Page の数値を適宜変更して上書き保存する。
手順5. 下側の欄の引数を参考にして Ghostscript を起動する (pdfmarks.txt のところに、作成したテキストファイルを指定する)

HTAプログラム>
<html>
<body>
<textarea id="src" cols=40 rows=30>
</textarea>
<input type="button" id="convert" value="convert ->"></input>
<textarea id="dest" cols=80 rows=30>
</textarea>
<br/>
<textarea id="cmd" cols=120 rows=3>
gswin32c -dBATCH -dNOPAUSE -sDEVICE=pdfwrite -sOutputFile=output.pdf input.pdf pdfmarks.txt
</textarea>
<script>
var txt_src = document.getElementById("src");
var txt_dest = document.getElementById("dest");
var btn_convert = document.getElementById("convert");
btn_convert.onclick = function(){
var lines = txt_src.value.split("\n");
var sz = lines.length;
for(var j = 0; j < sz; ++j){
var l_src = lines[j].replace("\r", "");
var l_dst = "\\376\\377"; // UTF16 (BE の BOM)
var ln = l_src.length;
for (var i = 0; i < ln; ++i){
var c = l_src.charCodeAt(i);
l_dst += "\\" + parseInt(c / 256).toString(8) + "\\" + (c & 255).toString(8);
}
lines[j] = "[/Page x /Count y /Title (" + l_dst + ") /OUT pdfmark";
}
txt_dest.value = lines.join("\n");
}
</script>

</body>
</html>

VS2008でTclVFSをtcl.8.5.15に対応させる方法 (VS2012、tcl 8.6.8 追記)

不具合現象

Tcl 8.5 系の現在の最新版である Tcl 8.5.15 と TclVFS 20080503 を VS2008 Express Edition 32bitモードでビルドしようとしたところ、下記エラーが出てしまいました。

<Tcl 8.5.15 + TclVFS 20080503 32bit モードでのビルドエラー内容>
===============================================================================
*** Compiler has 'Optimizations'
*** Compiler does not have 'Pentium 0x0f fix'
*** Linker has 'Win98 alignment problem'
*** Intermediate directory will be '.\Release_VC9\vfs_ThreadedDynamic'
*** Output directory will be '.\Release_VC9'
*** Suffix for binaries will be ''
*** Optional defines are '-DTCL_CFGVAL_ENCODING=\"cp1252\" -DSTDC_HEADERS -DTCL_THREADS=1 -DTCL_CFG_OPTIMIZED'
*** Compiler version 9. Target machine is IX86
*** Compiler options '-W3 -Ot -Oi -fp:strict -Gs -GS -GL -RTC1 -W3'
*** Link options '-ltcg'
c:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\INCLUDE\wtime.inl(37) : warning C4133: '関数' : 'const time_t *' と 'const __time32_t *' の間で型に互換性がありません。
c:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\INCLUDE\wtime.inl(43) : warning C4133: '関数' : 'const time_t *' と 'const __time32_t *' の間で型に互換性がありません。
c:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\INCLUDE\time.inl(32) : warning C4244: '関数' : 'time_t' から '__time32_t' への変換です。データが失われる可能性があります。
c:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\INCLUDE\time.inl(32) : warning C4244: '関数' : 'time_t' から '__time32_t' への変換です。データが失われる可能性があります。
c:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\INCLUDE\time.inl(38) : warning C4133: '関数' : 'const time_t *' と 'const __time32_t *' の間で型に互換性がありません。
c:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\INCLUDE\time.inl(44) : warning C4133: '関数' : 'const time_t *' と 'const __time32_t *' の間で型に互換性がありません。
c:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\INCLUDE\time.inl(51) : warning C4133: '関数' : 'const time_t *' と 'const __time32_t *' の間で型に互換性がありません。
c:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\INCLUDE\time.inl(57) : warning C4133: '関数' : 'const time_t *' と 'const __time32_t *' の間で型に互換性がありません。
c:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\INCLUDE\time.inl(64) : warning C4133: '関数' : 'const time_t *' と 'const __time32_t *' の間で型に互換性がありません。
c:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\INCLUDE\time.inl(69) : warning C4133: '関数' : 'const time_t *' と 'const __time32_t *' の間で型に互換性がありません。
c:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\INCLUDE\time.inl(81) : warning C4133: '関数' : 'time_t *' と '__time32_t *' の間で型に互換性がありません。
c:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\INCLUDE\sys/stat.inl(44) : error C2466: サイズが 0 の配列を割り当てまたは宣言しようとしました。
c:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\INCLUDE\sys/stat.inl(49) : error C2466: サイズが 0 の配列を割り当てまたは宣言しようとしました。
c:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\INCLUDE\sys/timeb.inl(46) : error C2466: サイズが 0 の配列を割り当てまたは宣言しようとしました。
c:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\INCLUDE\sys/utime.inl(39) : error C2466: サイズが 0 の配列を割り当てまたは宣言しようとしました。
c:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\INCLUDE\sys/utime.inl(44) : error C2466: サイズが 0 の配列を割り当てまたは宣言しようとしました。
c:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\INCLUDE\sys/utime.inl(49) : error C2466: サイズが 0 の配列を割り当てまたは宣言しようとしました。
c:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\INCLUDE\sys/utime.inl(78) : error C2466: サイズが 0 の配列を割り当てまたは宣言しようとしました。

また、多少エラー内容は違いますが、同じような現象が、Windows SDK .Net 3.5 SP1 (VC9 相当) の 64bitモードで Tcl 8.5.11 と TclVFS 20080503 を ビルドしても発生しました。

<Tcl 8.5.11 + TclVFS 20080503 64bit モードでのビルドエラー内容>
===============================================================================
*** Compiler has 'Optimizations'
*** Intermediate directory will be '.\Release_AMD64_VC9\vfs_ThreadedDynamic'
*** Output directory will be '.\Release_AMD64_VC9'
*** Suffix for binaries will be ''
*** Optional defines are '-DTCL_CFGVAL_ENCODING=\"cp1252\" -DSTDC_HEADERS -DTCL_THREADS=1 -DTCL_CFG_OPTIMIZED -DTCL_CFG_DO64BIT'
*** Compiler version 9. Target machine is AMD64
*** Compiler options '-W3 -Ot -Oi -fp:strict -Gs -GS -GL -RTC1 -W3'
*** Link options '-ltcg'
cl -DPACKAGE_NAME="\"vfs\"" -DPACKAGE_VERSION="\"1.3\"" -DBUILD_vfs -nologo -c -W3 -W3 -D _CRT_SECURE_NO_DEPRECATE -D _CRT_NONSTDC_NO_DEPRECATE -Fp.\Release_AMD64_VC9\vfs_ThreadedDynamic\ -DUSE_TCL_STUBS -Ot -Oi -fp:strict -Gs -GS -GL -MD -I"..\..\tcl8.5.15\generic" -I"..\..\tcl8.5.15\win" -I"..\win" -I"..\generic" -DTCL_CFGVAL_ENCODING=\"cp1252\" -DSTDC_HEADERS -DTCL_THREADS=1 -DTCL_CFG_OPTIMIZED -DTCL_CFG_DO64BIT -DBUILD_vfs -Fo.\Release_AMD64_VC9\vfs_ThreadedDynamic\ @C:\Users\keita\AppData\Local\Temp\nm6691.tmp
vfs.c
..\generic\vfs.c(1108) : error C2037: left of 'st_mode' specifies undefined struct/union '__stat64'
..\generic\vfs.c(1122) : error C2037: left of 'st_dev' specifies undefined struct/union '__stat64'
..\generic\vfs.c(1129) : error C2037: left of 'st_ino' specifies undefined struct/union '__stat64'
..\generic\vfs.c(1136) : error C2037: left of 'st_mode' specifies undefined struct/union '__stat64'
..\generic\vfs.c(1143) : error C2037: left of 'st_nlink' specifies undefined struct/union '__stat64'
..\generic\vfs.c(1150) : error C2037: left of 'st_uid' specifies undefined struct/union '__stat64'
..\generic\vfs.c(1157) : error C2037: left of 'st_gid' specifies undefined struct/union '__stat64'
..\generic\vfs.c(1164) : error C2037: left of 'st_size' specifies undefined struct/union '__stat64'
..\generic\vfs.c(1171) : error C2037: left of 'st_atime' specifies undefined struct/union '__stat64'
..\generic\vfs.c(1178) : error C2037: left of 'st_mtime' specifies undefined struct/union '__stat64'
..\generic\vfs.c(1185) : error C2037: left of 'st_ctime' specifies undefined struct/union '__stat64'
..\generic\vfs.c(1190) : error C2037: left of 'st_mode' specifies undefined struct/union '__stat64'
..\generic\vfs.c(1192) : error C2037: left of 'st_mode' specifies undefined struct/union '__stat64'
..\generic\vfs.c(1195) : error C2037: left of 'st_mode' specifies undefined struct/union '__stat64'

どうやら、 __stat64 をうまく認識できていないようです。

__stat64 は、 #include <sys/types.h> や #include <sys/stat.h> を C ソースに記述することで
使うことができるようになります。
Tcl では、#include "tclPort.h" の中で、 __stat64 を Tcl_StatBuf に typedef しています。

TclVFSのソース vfs.c を見てみると、先頭に
vfs.c ソースの先頭部分>
#include <tcl.h>
/* Required to access the 'stat' structure fields, and TclInExit() */
#include "tclInt.h"
#include "tclPort.h"

と書いてありますので、 Tcl_StatBuf (=__stat64) の定義をしようとはしているようです。

原因

わかりません...

対策

色々試してみたところ、 #include <tcl.h> よりも先に #include "tclPort.h" を記述することで
ビルドが通るようになりました。
つまり、 vfs.c の上記箇所を、下記のように変更すればOKです。

vfs.c ソース修正例>
#include "tclPort.h" /* ← tcl.h の前に移動してきた */
#include <tcl.h>
/* Required to access the 'stat' structure fields, and TclInExit() */
#include "tclInt.h"

<VS2012、Tcl 8.6.8では下記手順も追加>
1661行目 (VfsFileAttrStrings 関数定義の戻り値の型)を
static CONST char **

static CONST char * CONST86 *
と書き換え

この変更により、下記組み合わせのビルドがそれぞれ通るようになったことを確認済です。
  • Tcl 8.5.11 + TclVFS 20080503 64bit モード (Windows SDK .Net 3.5 SP1)
  • Tcl 8.5.15 + TclVFS 20080503 32bit モード (Visual Studio 2008 Express Edition)
  • Tcl 8.5.15 + TclVFS 20080503 64bit モード (Windows SDK .Net 3.5 SP1)
  • Tcl 8.6.8 + TclVFS 20080503 64bit モード (Visual Studio 2012 Express Edition)

std::lower_bound、std::upper_boundの関係をイラスト化してみた

ソート済配列に対して2分探索をしたり、昇順(または降順)の状態を維持したまま配列に要素を挿入したいといった場合、C++ではstd::lower_bound や std::upper_boundを活用すると便利ですが、それぞれが返すイテレータがどこを指すのか理解が曖昧だったので、イラスト化してみました。

 

<イラスト>
イメージ 1

Base16kエンコーダ/デコーダ

文字列にバイナリを埋め込む方法としてbase64が良く使われていますが、
バイナリを埋め込んだ文字列の 転送/格納にUTF16を使用する場合、
base64文字列をUTF16で扱うことになるため、上位8ビットが丸々0になり、
転送/格納効率が非常に悪くなります。

UTF16の場合は、base64のようにアルファベットや数字を使うよりも、
より多くの種類がある漢字を使った方が 効率が良くなるはずです。
というわけで、既存技術を探してみたところ、 base16kというものを発見しました。

base64では半角アルファベットや数字等、64種類の文字を使用するに対し、
base16kでは、Unicodeのうち、U+5000 - U+8fff の範囲にある、16384種類の文字を使用します。

この領域はCJK統合漢字と呼ばれるようで、みっちり漢字が詰まってます。

base16kエンコーダ/デコーダのサンプルプログラムを作ってみました。
boostライセンスとして公開します。


luaで、luaの関数を呼ぶC++の関数をluaから呼んでみた

引数2個、戻り値1個のlua関数を引数にとり、
実行されると与えられた引数の関数に5,15を入れて呼び出し、
得られた戻り値を2倍したものを返り値とするlua関数をC++で作成しました。

大変ややこしいですが、C++ => LUAの呼び出し、 LUA => C++ の呼び出しを
まとめて書ける豪華?なサンプル、ということで試しに作ってみました。

試した環境は LuaJIT-2.0.0、Visual Studio 2008です。

lua
print('callfunc:' ..
callfunc(
function(a, b)
print('Hello');
print('a=' .. a);
print('b=' .. b);
return a + b;
end
)
);

C++
#include <cstdio>
extern "C" {
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
}

int l_callfunc(lua_State *L){
luaL_checktype(L, -1, LUA_TFUNCTION);

lua_pushnumber(L, 5); // 第一引数
lua_pushnumber(L, 15); // 第二引数
lua_pcall(L, 2, 1, 0); // 引数2つ、戻り値1つとして積まれている関数を呼ぶ

double result = lua_tonumber(L, -1);
printf("result:%f\n", result);
lua_pop(L, 1);
lua_pushnumber(L, result * 2);
return 1; // 戻り値の数
}

int main(int argc, char *argv[]){
if (argc < 2) return 0;
lua_State *L = luaL_newstate();
luaL_openlibs(L);
lua_register(L, "callfunc", l_callfunc);
if (luaL_loadfile(L, argv[1])){
printf("load失敗\n");
return 1;
}
if (lua_pcall(L, 0, 0, 0)){
printf("実行失敗\n");
return 1;
}else{
printf("最後の戻り値:%f\n", lua_tonumber(L, -1));
}
lua_close(L);
return 0;
}

結果
Hello
a=5
b=15
result:20.000000
callfunc:40
最後の戻り値:0.000000

BLTのgraphのmarkerでグラフにお絵かき

コピーレフトのないオープンソースで、何か図を書いた後emfファイルに書き出す、
この要件を満たすものを探したところ、以前少し触れたBLTが使えることがわかりましたので、
やり方をメモします。

BLTのgraphを使うと、グラフをtcltkのウィンドウ上に描画できます。
また、graphコマンドのsnapコンポーネント(サブコマンド的な位置づけ?)で、
emfファイルに書き出したり、emfをクリップボードに入れることができるようです。

普通にグラフを書く方法は他のページにあるので、ここでは、graphコマンドのmarkerコンポーネント
で、axisが付いたグラフ内にお絵かきをしてみたいと思います。
サンプルコードは、「塗りつぶした四角形を描いてクリップボードにemfをコピーする」というものです。
markerを使えば、他にも、画像を追加したり、線を書いたりサブウィンドウをグラフの中に挿入
したりできるみたいです。
この方法を応用すれば、コンター図とか、ボロノイ図とか、2Dカラーマップとかも
描けるはずです(このあたりはもっと簡単にできる方法があるかもしれませんが…)。

サンプルコード
package require BLT
namespace import blt::*
wm title . "マーカーでお絵かき"

pack [set g [graph .g -plotbackground black]]
$g configure -title "マーカーでお絵かき"

$g axis configure x -title {X Axis} -min 0 -max 20
$g axis configure y -title {Y Axis} -min 0 -max 20
$g marker create polygon -fill red -coords { 5 5 5 10 10 10 10 5 }
$g marker create polygon -fill blue -coords { 10 5 10 10 15 10 15 5 }
$g marker create polygon -fill #404080 -coords { 15 5 15 10 20 10 20 5 }
$g marker create text -outline white -text {Hello world!} -rotate 10 -coords { 15 20 }

after 100 {$g snap -format emf CLIPBOARD}