OSSチャレンジPart2

目的

学んだことを忘れないようにメモ!

Part1はこっち ちょっとだけOSSデビューした - okadak1990’s blog

やったこと

Apache Arrow の C++に、Unicode正規化の機能を追加した。

github.com

こちらのコードに、正規化のロジックを追加する。

arrow/scalar_string.cc at master · apache/arrow · GitHub

Apache Arrow C++UTF-8文字列の処理にJulia由来のutf8procというライブラリーを使ってるので、こちらを利用する。

utf8proc/utf8proc.h at master · JuliaStrings/utf8proc · GitHub

Unicode正規化って?

Unicode文字って同じ文字でも、違うコードを持ってることがある。

例えば、「が」という文字は、「が(U+304C)」 or 「か(U+304B) と ゛(U+3099)」という2つの表し方がある。

これらがバラバラだと、文字比較できなくなるので、正規化して比較しようって時に使う。

4種類の比較方法がある。

NFD/NFKDとNFC/NFKCは、例えば「が」の場合、NFD/NFKDだと「か(U+304B) と ゛(U+3099)」になって、NFC/NFKCだと「が(U+304C)」になる。

NFC/NFD と NFKC/NFKDの違いは、例えば「ガ」の場合、NFC/NFDだと見た目上「ガ」で、 NFKC/NFKDだと見た目上「ガ」になる。

詳しくは下のリンクを見てください。

詰まったこと

開発したい場所のテストが実行できない

Building Arrow C++ — Apache Arrow v5.0.0

このURL通りに実行したら、テスト動くところまではすぐだったけど、今回直したい、「scalar_string_test」が実行されなかった。

原因は、上の方にある手順はあくまで最小限のbuildで必要なもので、optionが足りなかった。

-DARROW_COMPUTE=ON を追加すればできた。

Building Arrow C++ — Apache Arrow v5.0.0

今回は、scalar_string_test だけ実行したかったので、開発中は、テストコマンドに-R arrow-compute-scalar-test を追加すると、labelで絞り込みしてテストができた。

繰り返しで一文字ずつ入れようとしてみた

最初は下のコードのように、一文字づつunicode正規化を行って、その結果を返すようにしてみた。

struct Utf8NfkcTransform : public StringTransformBase {
  int64_t Transform(const uint8_t* input, int64_t input_string_ncodeunits,
                    uint8_t* output) {

    for (const uint8_t* c = input; c < input + input_string_ncodeunits; ++c) {
      uint8_t* o = utf8proc_NFKC(c);
      *output++ = *o;
    }

    return input_string_ncodeunits;
  }
};

このコードが間違えてる理由は次の通り

  • c には、1byteが入ってるけど、utf-8文字は1文字(コードポイントというらしい)が1byteとは限らない。だから、「①」(のポインター)がutf8proc_NFKCに渡されるのでなく、「①」を表す文字の最初の1byteのポインターが渡される。
  • 正規化した文字が、2文字になる可能性があるので、このようにoutputに1文字づつ追加するという考えは間違えている。(そもそも文字とbyte数の概念から意識できてない)
  • utf8proc_NFKCは、NULL終端文字列を受け取るようになっている。最初の一文字のpointerを受け取ると、そこからNULL終端文字があるまでの文字列を変換してくれる。今回は、変換したい文字列の区切りにNULL終端文字を使ってないので、今回のケースでは使えない。

このロジックは、「1234」みたいな1byte文字だと想定通りの挙動を示すけど、それ以外だと上手く行かない。

正規化後の文字のbyte数がわからない

utf8proc_NFKCでなく、utf8proc_mapを使うようにとアドバイスをもらったので書き換えてみた。

utf8proc/utf8proc.c at master · JuliaStrings/utf8proc · GitHub

struct Utf8NfkcTransform : public StringTransformBase {
  int64_t Transform(const uint8_t* input, int64_t input_string_ncodeunits,
                    uint8_t* output) {

    uint8_t* output_start = output;

    // ここの型が上手く解決できず、static_castというやり方を教えてもらってできた...
    utf8proc_option_t options = static_cast<utf8proc_option_t>(UTF8PROC_STABLE | UTF8PROC_COMPOSE | UTF8PROC_COMPAT);
    utf8proc_map(input, input_string_ncodeunits, &output, options);

    return output - output_start;
  }
};

このコードが間違えてる理由は次の通り

  • utf8proc_mapを通すと、outputのアドレスが変わっていた。そのため、output - output_start というponterのアドレスの引き算では、byte数が上手く取得できなかった。(「1435477920」みたいな全然違う値が返ってきた。)
  • 最初のoutputが持つアドレスの場所に変換後の文字を入れなければいけないのに、utf8proc_mapを通すことで、outputという変数が指し示すアドレスの位置が変わってしまい、想定したアドレスの場所に値を入れられなかった。

結局正解を教えてもらった...

struct Utf8NfTransformBase : public StringTransformBase {
  int64_t Transform(const uint8_t* input, int64_t input_string_ncodeunits,
                    uint8_t* output) {
    utf8proc_uint8_t* transformed = nullptr;
 auto options = static_cast<utf8proc_option_t>(UTF8PROC_STABLE | UTF8PROC_COMPOSE | UTF8PROC_COMPAT);
    auto output_string_ncodeunits =
        utf8proc_map(input, input_string_ncodeunits, &transformed, options);
    std::memcpy(output, transformed, output_string_ncodeunits);
    free(transformed);

    return output_string_ncodeunits;
  }
};

学んだこと

  • そもそもutf8_proc_map はsize_t(文字列のbyte数)を返してくれるので、それをそのまま返り値にすればよかった...。output - output_start をしてみたが、0が返ってきた。多分最初は、++output = *oみたいにpointerの指し示す位置を文字数分進めてたから、上手く動いたのだと思われる。
  • 一時的な変数に正規化の結果を入れて、それをoutputにcopyする処理が必要。こうすることで、outputが指し示す元のアドレスに対して、値を格納できる。

感想

今までponterとかふわっと理解したつもりだったけど、全然理解してなかったんだなと思った。

ほんとに学びが多い。すごく楽しい。