Python ctypes で char * を読む

Python ctypes モジュールは C で書かれたライブラリにアクセスできる。しかしポインタ周りが分かりにくい。
freetype-py (Python - FreeType) で OpenType フォントのグリフ置換テーブル (GSUB, Glyph Substitution Table) を読み込もうとしたときの記録。

FreeType は GPOS などのテーブルへアクセスする関数 FT_OpenType_Validate を提供しているが、テーブルのデータをメモリ内に読み込んだ位置、つまりポインタとして返してくれる。

  FT_EXPORT( FT_Error )
  FT_OpenType_Validate( FT_Face    face,
                        FT_UInt    validation_flags,
                        FT_Bytes  *BASE_table,
                        FT_Bytes  *GDEF_table,
                        FT_Bytes  *GPOS_table,
                        FT_Bytes  *GSUB_table,
                        FT_Bytes  *JSTF_table );
http://www.freetype.org/freetype2/docs/reference/ft2-ot_validation.html#FT_OpenType_Validate

FT_Bytes * (const unsigned char *) で NULL 終端文字列や長さの指定はないので、byref で文字列参照や配列として一度に取得できない。
文字列として参照しようとすると、NULL 終端文字列として判断され、Python の文字列に変換されてしまう。参照としてアクセスしようとすると None が返ることがあるが、これは 0 が Python の None に変換されているせいで、とても使えない。
配列として読み込もうとすると長さを指定しなければいけない。

この場合、GSUB_table 変数として渡す値を pointer(FT_Bytes()) としてやり、値にアクセスするときは POINTER(c_char) にキャストしてから行う。以下にコードの一部を抜き出しておいた。

import struct
from freetype import *
gsub_table_p = pointer(FT_Bytes())
# calling FT_OpenType_Validate function here
if gsub_table_p.contents.value is None: # NULL pointer check
    raise Exception("GSUB Table not found")
p = cast(gsub_table_p.contents, POINTER(c_char))
print("Version: " + hex(struct.unpack(">I", p[0:4])))
# 0x1000

ポインタの gsub_table_p.contents.value が None の場合は NULL ポインタなので、アクセスしようとしたテーブルが存在しない、またはフラグでアクセス指定していない。
const unsigned char * ポインタから中身へのアクセスはスライスで文字列として行えるため、struct モジュールの unpack が利用できる。
GSUB テーブルの最初の4バイトを読みだしてバージョンを確認しているが、OpenType フォントのデータはビッグエンディアンなので、">I" として unpack している。

All OpenType fonts use Motorola-style byte ordering (Big Endian):

https://www.microsoft.com/typography/otspec/otff.htm

もちろん後は C で読むのと同じ。キャストするときに型の指定を PONTER(c_ubyte) とすると読み出しはリストとして返される。

以下は関数宣言部分。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from freetype import *
from freetype.raw import _lib
FT_OpenType_Validate = _lib.FT_OpenType_Validate
FT_OpenType_Validate.argtypes = (FT_Face, FT_UInt, POINTER(FT_Bytes), 
    POINTER(FT_Bytes), POINTER(FT_Bytes), POINTER(FT_Bytes), POINTER(FT_Bytes))
FT_OpenType_Free = _lib.FT_OpenType_Free
FT_OpenType_Free.argtypes = (FT_Face, FT_Bytes)

FT_VALIDATES = {
    "FT_VALIDATE_BASE": 0x0100, 
    "FT_VALIDATE_GDEF": 0x0200, 
    "FT_VALIDATE_GPOS": 0x0400, 
    "FT_VALIDATE_GSUB": 0x0800, 
    "FT_VALIDATE_JSTF": 0x1000, 
    "FT_VALIDATE_MATH": 0x2000, 
    "FT_VALIDATE_OT": 0x01000 | 0x0200 | 0x0400 | 0x0800 | 0x1000 | 0x200
}
globals().update(FT_VALIDATES)

freetype-py[1] でも横書きのグリフを縦書のグリフに置換することができるようになった。

1: https://github.com/rougier/freetype-py/

おまけ

const unsigned char * を構造体にキャストして読み込もうとした。ポインタをインクリメントするために c_void_p にキャストする必要があるが、キャストしたらアドレスが変わってしまった。

>> print(p)
<__main__.LP_c_char_p object at 0x7f8d70861440>
>> pp = cast(p, c_void_p)
0x7f8d70861400