node-ffi wchar_t

障害

「のでーっふぃ」とかカワイイ...「ノードエフエフアイ」つまり、Node.jsがらみのForeign function interfaceです。C++ DLLとjavascriptをつなぐモジュールです。が、wchar_tがC++にうまく渡りません。困りました。

経緯

関連スペックは次のようになります。

テスト用のC++DLL関数は、次のように、引数がwchar_tです。

 size_t fnsub(const wchar_t* txt);


また、typesriptからは、次のように接続、呼び出しをします。

var ref = require('ref');
var ffi = require('ffi');
var wchar_t = require('ref-wchar');

var mylib = ffi.Library('sub', {
    'fnsub': ['int', [wchar_t]],
});

var ret = mylib.fnsub('愛あればこそ。');


結果は、不正アクセスで異常終了です。困りました。

対策

ref-wcharをあきらめ、同一作者のテストコードwchar_t.jsを改変して使いました。
Nathan Rajlich@TooTallNate's wchar_t.js

改変したwchar_t.js

// original author: Nathan Rajlich@TooTallNate
var ref = require('ref');
var ffi = require('ffi'); // added by eggtoothcroc
var Iconv = require('iconv').Iconv;

// vars used by the "wchar_t" type
var wchar_size, getter, setter;

// On Windows they're UTF-16 (2-bytes), but on Unix platform they're UTF-32
// (4-bytes).
if ('win32' == process.platform) {
    wchar_size = 2;
    getter = new Iconv('UTF-16' + ref.endianness, 'UTF-8');
    setter = new Iconv('UTF-8', 'UTF-16' + ref.endianness);
   } else {
    wchar_size = 4;
    getter = new Iconv('UTF-32' + ref.endianness, 'UTF-8');
    setter = new Iconv('UTF-8', 'UTF-32' + ref.endianness);
}

// Create a "wchar_t *" type. We use the "CString" type as a base since it's pretty
// close to what we actually want. We just have to define custom "get" and "set"
// functions and then we can use this type in FFI functions.
var wchar_t = Object.create(ref.types.CString);

wchar_t.get = function get(buf, offset) {
    var _buf = buf.readPointer(offset);
    if (_buf.isNull()) {
        return null;
    }
    var stringBuf = _buf.reinterpretUntilZeros(wchar_size);
    return getter.convert(stringBuf).toString('utf8');
};

wchar_t.set = function set(buf, offset, val) {
    var _buf = val; // val is a Buffer? it better be \0 terminated...
    if ('string' == typeof val) {
        _buf = setter.convert(val + '\0');
    }
    return buf.writePointer(_buf, offset);
};

wchar_t.ffi_type = ffi.FFI_TYPES.pointer;  // added by eggtoothcroc
module.exports = wchar_t;        // added by eggtoothcroc


テスト用javascript (app.ts)

var ref = require('ref');
var ffi = require('ffi');
var wchar_t = require('./wchar_t');

var mylib = ffi.Library('sub', {
    'fnsub': ['int', [wchar_t]],
});

var ret = mylib.fnsub('00愛0');
console.log('  len=', ret);

テスト用C++DLLヘッダ (sub.h)

#ifdef SUB_EXPORTS
#define SUB_API __declspec(dllexport)
#else
#define SUB_API __declspec(dllimport)
#endif

extern "C"{
	SUB_API size_t fnsub(const wchar_t* txt);
}

テスト用C++DLLソース (sub.cpp)

#include "stdafx.h"
#include <iostream>
#include <iomanip>
#include "sub.h"

SUB_API size_t fnsub(const wchar_t* txt)
{
	setlocale(LC_ALL, "japanese");
	std::wcout << "txt=" << txt << std::endl;
	std::cout
		<< std::hex << std::setfill('0') << std::setw(4) << (unsigned)txt[0] <<" "
		<< std::hex << std::setfill('0') << std::setw(4) << (unsigned)txt[1] <<" "
		<< std::hex << std::setfill('0') << std::setw(4) << (unsigned)txt[2] <<" "
		<< std::hex << std::setfill('0') << std::setw(4) << (unsigned)txt[3] <<" "
		<< std::hex << std::setfill('0') << std::setw(4) << (unsigned)txt[4] <<" "
		<< std::endl;
	return wcslen(txt);
}

実行結果

txt=00愛0
0030 0030 611b 0030 0000
  len= 4

補足

文字コードについて
javascript内部(string) UTF-16
FFI変換 UTF-16 -> UTF-8
C++内部(wchar_t) UTF-16LE

javascript内部文字コードのendiannessがBig EndianかLittle Endianかは解りませんが、FFIを通すと、外部に対してUTF-8になるようです。で、wchar_tはUTF-16LEなので、変換(UTF-8 -> UTF-16LE)しなければならないようです。

改変部分抜粋

オリジナルのwchar_t.jsから必須部分を抜き出すと次のようなコードになります。

var ref = require('ref');
var ffi = require('ffi');
var Iconv = require('iconv').Iconv;

var setter = new Iconv('UTF-8', 'UTF-16LE');

var wchar_t = {
    size: 8,
    indirection: 1,
    ffi_type : ffi.FFI_TYPES.pointer,
    get: function get(buf, offset) { return 'foo'; },
    set: function set(buf, offset, val) {
        var _buf = val;
        if ('string' == typeof val) {
            _buf = setter.convert(val + '\0');
        }
        return buf.writePointer(_buf, offset);
    }
}

module.exports = wchar_t;


重要なのは、ffi_typeの付加です。次のようにも書けます。

  wchar_t.ffi_type =ffi.FFI_TYPES.pointer;


このコードはあくまでも見通しを良くする為のもので、getは機能しません。エンディアンネスも考慮していません。このコードも含め、今回のコード改変は、FFIの機構を理解しないままで、正当性は二の次にして、動くということを試したものです。悪しからず。