SmallBoltでUFFI入門
PharoからCの共有ライブラリを使う
2020 Smalltalk勉強会
Masashi Umezawa
UFFIとは?
● Unified FFI
○ 外部関数呼び出しインターフェース (FFI)
■ Squeak, Pharoで複数あったFFIを統一化したもの
○ 共有ライブラリの公開関数を呼びだせる
○ コールバックを受けることも可能
● 既に便利なライブラリが存在する時に有効
● 速度が気になる低レイヤの処理をさせるにも便利
チュートリアル
● "Unified FFI - Calling Foreign Functions from Pharo"
○ Pharo Booksの一つ
○ 基本的なcall outはできるようになる
○ callback(call in)の解説はない
UFFI適用例
● "SmallBolt"
○ Neo4j Boltドライバ
■ 定番Graph DBのNeo4jにアクセスするためのドライバ
■ Cで書かれたBoltプロトコルのライブラリSeaboltをFFIで
呼び出している
■ 従来のREST(http)によるドライバ(Neo4reSt)に比べて
3倍程度速い
SmallBoltのインストール
Metacello new
baseline: 'SmallBolt';
repository: 'github://mumez/SmallBolt';
load.
● Metacelloで入れる
● 共有ライブラリファイル(libseabolt)もダウンロードされる
○ イメージファイルのディレクトリにファイルが置かれるのでPharo
からロード可能
Neo4jのインストール
● Neo4j Download Centerにアクセス
○ https://neo4j.com/download-center/#community
■ tar.gzかzipを取得して展開
■ bin以下のneo4j(.bat)を起動
> neo4j console
SbSeaboltFFIをブラウズ
Object subclass: #SbSeaboltFFI
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: 'SbBoltConstants'
package: 'SmallBolt-FFI'
● FFI呼び出し用のメソッドがまとめられたクラス
○ 外部ライブラリファイルごとにクラスを作成することが多い
■ 今回はlibseaboltの関数群が対象
● SbBoltConstants
プールについては
後述
最初の例: SbSeaboltFFI >> startup
startup
^ self ffiCall: #( void Bolt_startup() )
● 引数も戻り値もない関数の例
○ void Bolt_startup() を呼び出すための記述
SbSeaboltFFI new startup
○ Playgroundで以下のようにして呼び出せる
共有ライブラリファイルへの参照はどこ?
FFILibrary subclass: #SbSeaboltLibrary
instanceVariableNames: ''
classVariableNames: ''
package: 'SmallBolt-FFI'
● FFILibraryのサブクラスを作成してファイル名などを記述
○ win32LibraryName,
macLibraryName,
unixLibraryName
のメソッドでファイル名
(libseabolt.dllなど)を返す
ffiLibraryName
^ SbSeaboltLibrary
● SbSeaboltFFI>>ffiLibraryName で上記クラスを指定
○ ファイル名を意識せずに
外部関数を呼ぶことが可能
になる
ロードされた共有ライブラリ群を確認
Smalltalk vm listLoadedModules
○ libseabolt.dllなどが含まれる
デフォルトでは
● VMのプラグインディレクトリ
● イメージの置かれているディレクトリ
● 環境変数PATH、LD_LIBRARY_PATHで
指定したディレクトリ
で検索が行われる
引数や戻り値のある呼び出し (1)
BoltValue* BoltAuth_basic(const char* username,
const char* password, const char* realm)
● BoltAuth_basicで認証してみる
○ Cのシグネチャ
● BoltValue* や const char* の扱いはどうなるのか?
引数や戻り値のある呼び出し (2)
authBasic: userName password: password realm: realm
^ self ffiCall: #( SbBoltValue BoltAuth_basic(String
userName, String password, String realm) )
● SbSeaboltFFI >> authBasic:password:realm: として定義
● Cの型はSmalltalkのクラスとしてマッピングできる
○ BoltValue* => SbBoltValue (FFIExternalObjectのサブクラス)
○ const char* => String (マッピングがあらかじめ定義されている)
引数や戻り値のある呼び出し (3)
boltValue := SbSeaboltFFI new
authBasic:'neo4j' password:'neoneo' realm: nil.
credentials := boltValue value.
boltValue destroy.
credentials inspect.
● 通常のSmalltalkのメッセージ送信で呼び出せる
○ C <-> Smalltalk の変換が自動的に行われる
ポインタ渡しによる書き換え (1)
int32 BoltValue_to_string(SbBoltValue aBoltValue, char* dest, int32
length, SbBoltConnection aSbBoltConnection)
● BoltValue_to_stringでSmalltalk側からポインタを渡すには?
○ Cのシグネチャ
○ SbSeaboltFFI >>
toString:fromValue:length:onConnection:
onConnection::password:realm:
toString: dest fromValue: aBoltValue length: length onConnection:
aSbBoltConnection
^ self ffiCall: #( int32 BoltValue_to_string(SbBoltValue aBoltValue, char*
dest, int32 length, SbBoltConnection aSbBoltConnection) )
ポインタ渡しによる書き換え (2)
debugShortString
| strAddress size |
strAddress := (ExternalAddress allocate: 64) autoRelease.
size := self ffi toString: strAddress fromValue: self length: 64
onConnection: SbBoltConnection null.
^ strAddress readString
● SbBoltValue >> debugShortString で使用
○ SmalltalkのExternalAddressでCのヒープにallocate
○ ポインタを受け取ったCが書き込み
○ Smalltalkで値を得る
ポインタ渡しによる書き換え (3)
boltValue := SbSeaboltFFI new authBasic:'neo4j' password:'neoneo' realm: nil.
boltValue debugString inspect.
● SbBoltValue>>debugShortStringを送ってみる
○ C側で生成されたデバッグ用文字列を得られる
コールバックの起動 (1)
● C側に関数ポインタにマッピングされるSmalltalkの
オブジェクトを渡す
○ Smalltalk側で設定したブロックが実行される
● FFICallbackのサブクラスを作成
○ SbBoltLogCallback
■ ライブラリ
からのLogを
受ける
FFICallback subclass: #SbBoltLogCallback
instanceVariableNames: ''
classVariableNames: ''
package: 'SmallBolt-FFI'
コールバックの起動 (2)
● C側の関数ポインタの定義
typedef void (* log_func)(void* state, const char* message);
● SbBoltLogCallbackにクラスメソッドを定義
fnSpec
^ 'void ( void* state, String message )'
on: aBlock
^ self signature: self fnSpec block: aBlock
コールバックの起動 (3)
● Cのデバッグログ設定用関数
void BoltLog_set_debug_func(BoltLog* log, log_func func);
● SbSeaboltFFI >> setDebugCallback:onLog:
setDebugCallback: aBoltLogCallback onLog: aBoltLog
^ self ffiCall: #(void BoltLog_set_debug_func(SbBoltLog
aBoltLog, SbBoltLogCallback aBoltLogCallback))
○ 関数ポインタ部分がSbBoltLogCallbackとなっている
Cからのコールバックの起動 (4)
● Debugログ出力のコールバックを設定してみる
client := SbClient new.
[ client configBuilder: [ :config :settings |
config log: (client libraryLogger debugCallback:
(SbBoltLogCallback on: [ :state :message | self traceCr: message ]))].
client connect.
] ensure: [ client release]
○ connect時の
ログが表示
される
定数の利用 (1)
SharedPool subclass: #SbBoltConstants
instanceVariableNames: ''
classVariableNames: 'BOLT_ACCESS_MODE_READ
BOLT_ACCESS_MODE_WRITE BOLT_CONNECTION_STATE_CONNECTED
BOLT_CONNECTION_STATE_DEFUNCT BOLT_CONNECTION_STATE_DISCONNECTED
BOLT_CONNECTION_STATE_FAILED BOLT_CONNECTION_STATE_READY
BOLT_SCHEME_DIRECT BOLT_SCHEME_NEO4J BOLT_TRANSPORT_ENCRYPTED
BOLT_TRANSPORT_PLAINTEXT'
package: 'SmallBolt-FFI'
● Cでtypedefされている定数
○ Smalltalk側ではSharedPoolとして表す
定数の利用 (2)
initialize
BOLT_TRANSPORT_PLAINTEXT := 0.
BOLT_TRANSPORT_ENCRYPTED := 1.
...
● SbBoltConstants class >> initialize 経由で初期化
usePlainTextTransport
self ffi setTransport: BOLT_TRANSPORT_PLAINTEXT onConfig: self
● SbBoltConfig >> usePlainTextTransport
○ Constantsを使う指定をしておけば、マジックナンバーを
書かずに済む
Enumの利用 (1)
FFIEnumeration subclass: #SbBoltType
...
● FFIEnumerationのサブクラスを作成する
○ クラスメソッド enumDecl をCの定義に合わせて追加
○ SbBoltType initializeでプールの初期化が行われる
Enumの利用 (2)
enumDecl
^ #(BOLT_NULL 0
BOLT_BOOLEAN 1
BOLT_INTEGER 2
BOLT_FLOAT 3
BOLT_STRING 4
BOLT_DICTIONARY 5
BOLT_LIST 6
BOLT_BYTES 7
BOLT_STRUCTURE 8)
● SbBoltType class >> enumDecl
FFIEnumeration subclass: #SbBoltType
instanceVariableNames: ''
classVariableNames: 'BOLT_BOOLEAN
BOLT_BYTES BOLT_DICTIONARY BOLT_FLOAT
BOLT_INTEGER BOLT_LIST BOLT_NULL
BOLT_STRING BOLT_STRUCTURE'
package: 'SmallBolt-FFI'
○ SbBoltType initializeすると変数が
定義され初期化されている
Enumの利用 (3)
enum BoltType BoltValue_type(const BoltValue* value);
● SbSeaboltFFI >> getTypeOnValue:
getTypeOnValue: aBoltValue
^ self ffiCall: #( SbBoltType BoltValue_type(SbBoltValue aBoltValue) )
● BoltValue_typeでenumを返している
○ SbBoltTypeのインスタンスが返される
■ item (シンボルが返る)
■ value (値が返る) などのメッセージに答えられる
Enumの利用 (4)
boltValue := SbSeaboltFFI new
authBasic: 'neo4j' password: 'neoneo' realm: nil.
type := boltValue type.
{type item. type value} inspect.
● BoltValue >> type 経由でgetTypeOnValue:を呼んでみる
その他UFFIの適用例
● Pharo-SQLite3
○ SQLiteへのコネクタ
● PunQLite
○ UnQLite(KVS)へのコネクタ
● libtensorflow-pharo-bindings
○ TensorFlowバインディング
● pharo-protobuf
○ gRPC及びProtocol Buffersのサポート
UFFI向けお勧めライブラリ
● libvips
○ 高速な画像処理がPharoで可能に!
● librdkafka
○ Kafkaにつないで分散ストリーム処理
などなど
まとめ
● Unified FFIを使うと既存のCの共有ライブラリを使ってPharo
を拡張できる
● C <-> Smalltalkのマッピングが定義されており楽
● コールアウト、コールインの両方がサポートされている
いろいろなライブラリを呼び出してPharoを気軽に拡張してい
きましょう!

SmalltalkBoltでUFFI入門

  • 1.
  • 2.
    UFFIとは? ● Unified FFI ○外部関数呼び出しインターフェース (FFI) ■ Squeak, Pharoで複数あったFFIを統一化したもの ○ 共有ライブラリの公開関数を呼びだせる ○ コールバックを受けることも可能 ● 既に便利なライブラリが存在する時に有効 ● 速度が気になる低レイヤの処理をさせるにも便利
  • 3.
    チュートリアル ● "Unified FFI- Calling Foreign Functions from Pharo" ○ Pharo Booksの一つ ○ 基本的なcall outはできるようになる ○ callback(call in)の解説はない
  • 4.
    UFFI適用例 ● "SmallBolt" ○ Neo4jBoltドライバ ■ 定番Graph DBのNeo4jにアクセスするためのドライバ ■ Cで書かれたBoltプロトコルのライブラリSeaboltをFFIで 呼び出している ■ 従来のREST(http)によるドライバ(Neo4reSt)に比べて 3倍程度速い
  • 5.
    SmallBoltのインストール Metacello new baseline: 'SmallBolt'; repository:'github://mumez/SmallBolt'; load. ● Metacelloで入れる ● 共有ライブラリファイル(libseabolt)もダウンロードされる ○ イメージファイルのディレクトリにファイルが置かれるのでPharo からロード可能
  • 6.
    Neo4jのインストール ● Neo4j DownloadCenterにアクセス ○ https://neo4j.com/download-center/#community ■ tar.gzかzipを取得して展開 ■ bin以下のneo4j(.bat)を起動 > neo4j console
  • 7.
    SbSeaboltFFIをブラウズ Object subclass: #SbSeaboltFFI instanceVariableNames:'' classVariableNames: '' poolDictionaries: 'SbBoltConstants' package: 'SmallBolt-FFI' ● FFI呼び出し用のメソッドがまとめられたクラス ○ 外部ライブラリファイルごとにクラスを作成することが多い ■ 今回はlibseaboltの関数群が対象 ● SbBoltConstants プールについては 後述
  • 8.
    最初の例: SbSeaboltFFI >>startup startup ^ self ffiCall: #( void Bolt_startup() ) ● 引数も戻り値もない関数の例 ○ void Bolt_startup() を呼び出すための記述 SbSeaboltFFI new startup ○ Playgroundで以下のようにして呼び出せる
  • 9.
    共有ライブラリファイルへの参照はどこ? FFILibrary subclass: #SbSeaboltLibrary instanceVariableNames:'' classVariableNames: '' package: 'SmallBolt-FFI' ● FFILibraryのサブクラスを作成してファイル名などを記述 ○ win32LibraryName, macLibraryName, unixLibraryName のメソッドでファイル名 (libseabolt.dllなど)を返す ffiLibraryName ^ SbSeaboltLibrary ● SbSeaboltFFI>>ffiLibraryName で上記クラスを指定 ○ ファイル名を意識せずに 外部関数を呼ぶことが可能 になる
  • 10.
    ロードされた共有ライブラリ群を確認 Smalltalk vm listLoadedModules ○libseabolt.dllなどが含まれる デフォルトでは ● VMのプラグインディレクトリ ● イメージの置かれているディレクトリ ● 環境変数PATH、LD_LIBRARY_PATHで 指定したディレクトリ で検索が行われる
  • 11.
    引数や戻り値のある呼び出し (1) BoltValue* BoltAuth_basic(constchar* username, const char* password, const char* realm) ● BoltAuth_basicで認証してみる ○ Cのシグネチャ ● BoltValue* や const char* の扱いはどうなるのか?
  • 12.
    引数や戻り値のある呼び出し (2) authBasic: userNamepassword: password realm: realm ^ self ffiCall: #( SbBoltValue BoltAuth_basic(String userName, String password, String realm) ) ● SbSeaboltFFI >> authBasic:password:realm: として定義 ● Cの型はSmalltalkのクラスとしてマッピングできる ○ BoltValue* => SbBoltValue (FFIExternalObjectのサブクラス) ○ const char* => String (マッピングがあらかじめ定義されている)
  • 13.
    引数や戻り値のある呼び出し (3) boltValue :=SbSeaboltFFI new authBasic:'neo4j' password:'neoneo' realm: nil. credentials := boltValue value. boltValue destroy. credentials inspect. ● 通常のSmalltalkのメッセージ送信で呼び出せる ○ C <-> Smalltalk の変換が自動的に行われる
  • 14.
    ポインタ渡しによる書き換え (1) int32 BoltValue_to_string(SbBoltValueaBoltValue, char* dest, int32 length, SbBoltConnection aSbBoltConnection) ● BoltValue_to_stringでSmalltalk側からポインタを渡すには? ○ Cのシグネチャ ○ SbSeaboltFFI >> toString:fromValue:length:onConnection: onConnection::password:realm: toString: dest fromValue: aBoltValue length: length onConnection: aSbBoltConnection ^ self ffiCall: #( int32 BoltValue_to_string(SbBoltValue aBoltValue, char* dest, int32 length, SbBoltConnection aSbBoltConnection) )
  • 15.
    ポインタ渡しによる書き換え (2) debugShortString | strAddresssize | strAddress := (ExternalAddress allocate: 64) autoRelease. size := self ffi toString: strAddress fromValue: self length: 64 onConnection: SbBoltConnection null. ^ strAddress readString ● SbBoltValue >> debugShortString で使用 ○ SmalltalkのExternalAddressでCのヒープにallocate ○ ポインタを受け取ったCが書き込み ○ Smalltalkで値を得る
  • 16.
    ポインタ渡しによる書き換え (3) boltValue :=SbSeaboltFFI new authBasic:'neo4j' password:'neoneo' realm: nil. boltValue debugString inspect. ● SbBoltValue>>debugShortStringを送ってみる ○ C側で生成されたデバッグ用文字列を得られる
  • 17.
    コールバックの起動 (1) ● C側に関数ポインタにマッピングされるSmalltalkの オブジェクトを渡す ○Smalltalk側で設定したブロックが実行される ● FFICallbackのサブクラスを作成 ○ SbBoltLogCallback ■ ライブラリ からのLogを 受ける FFICallback subclass: #SbBoltLogCallback instanceVariableNames: '' classVariableNames: '' package: 'SmallBolt-FFI'
  • 18.
    コールバックの起動 (2) ● C側の関数ポインタの定義 typedefvoid (* log_func)(void* state, const char* message); ● SbBoltLogCallbackにクラスメソッドを定義 fnSpec ^ 'void ( void* state, String message )' on: aBlock ^ self signature: self fnSpec block: aBlock
  • 19.
    コールバックの起動 (3) ● Cのデバッグログ設定用関数 voidBoltLog_set_debug_func(BoltLog* log, log_func func); ● SbSeaboltFFI >> setDebugCallback:onLog: setDebugCallback: aBoltLogCallback onLog: aBoltLog ^ self ffiCall: #(void BoltLog_set_debug_func(SbBoltLog aBoltLog, SbBoltLogCallback aBoltLogCallback)) ○ 関数ポインタ部分がSbBoltLogCallbackとなっている
  • 20.
    Cからのコールバックの起動 (4) ● Debugログ出力のコールバックを設定してみる client:= SbClient new. [ client configBuilder: [ :config :settings | config log: (client libraryLogger debugCallback: (SbBoltLogCallback on: [ :state :message | self traceCr: message ]))]. client connect. ] ensure: [ client release] ○ connect時の ログが表示 される
  • 21.
    定数の利用 (1) SharedPool subclass:#SbBoltConstants instanceVariableNames: '' classVariableNames: 'BOLT_ACCESS_MODE_READ BOLT_ACCESS_MODE_WRITE BOLT_CONNECTION_STATE_CONNECTED BOLT_CONNECTION_STATE_DEFUNCT BOLT_CONNECTION_STATE_DISCONNECTED BOLT_CONNECTION_STATE_FAILED BOLT_CONNECTION_STATE_READY BOLT_SCHEME_DIRECT BOLT_SCHEME_NEO4J BOLT_TRANSPORT_ENCRYPTED BOLT_TRANSPORT_PLAINTEXT' package: 'SmallBolt-FFI' ● Cでtypedefされている定数 ○ Smalltalk側ではSharedPoolとして表す
  • 22.
    定数の利用 (2) initialize BOLT_TRANSPORT_PLAINTEXT :=0. BOLT_TRANSPORT_ENCRYPTED := 1. ... ● SbBoltConstants class >> initialize 経由で初期化 usePlainTextTransport self ffi setTransport: BOLT_TRANSPORT_PLAINTEXT onConfig: self ● SbBoltConfig >> usePlainTextTransport ○ Constantsを使う指定をしておけば、マジックナンバーを 書かずに済む
  • 23.
    Enumの利用 (1) FFIEnumeration subclass:#SbBoltType ... ● FFIEnumerationのサブクラスを作成する ○ クラスメソッド enumDecl をCの定義に合わせて追加 ○ SbBoltType initializeでプールの初期化が行われる
  • 24.
    Enumの利用 (2) enumDecl ^ #(BOLT_NULL0 BOLT_BOOLEAN 1 BOLT_INTEGER 2 BOLT_FLOAT 3 BOLT_STRING 4 BOLT_DICTIONARY 5 BOLT_LIST 6 BOLT_BYTES 7 BOLT_STRUCTURE 8) ● SbBoltType class >> enumDecl FFIEnumeration subclass: #SbBoltType instanceVariableNames: '' classVariableNames: 'BOLT_BOOLEAN BOLT_BYTES BOLT_DICTIONARY BOLT_FLOAT BOLT_INTEGER BOLT_LIST BOLT_NULL BOLT_STRING BOLT_STRUCTURE' package: 'SmallBolt-FFI' ○ SbBoltType initializeすると変数が 定義され初期化されている
  • 25.
    Enumの利用 (3) enum BoltTypeBoltValue_type(const BoltValue* value); ● SbSeaboltFFI >> getTypeOnValue: getTypeOnValue: aBoltValue ^ self ffiCall: #( SbBoltType BoltValue_type(SbBoltValue aBoltValue) ) ● BoltValue_typeでenumを返している ○ SbBoltTypeのインスタンスが返される ■ item (シンボルが返る) ■ value (値が返る) などのメッセージに答えられる
  • 26.
    Enumの利用 (4) boltValue :=SbSeaboltFFI new authBasic: 'neo4j' password: 'neoneo' realm: nil. type := boltValue type. {type item. type value} inspect. ● BoltValue >> type 経由でgetTypeOnValue:を呼んでみる
  • 27.
    その他UFFIの適用例 ● Pharo-SQLite3 ○ SQLiteへのコネクタ ●PunQLite ○ UnQLite(KVS)へのコネクタ ● libtensorflow-pharo-bindings ○ TensorFlowバインディング ● pharo-protobuf ○ gRPC及びProtocol Buffersのサポート
  • 28.
    UFFI向けお勧めライブラリ ● libvips ○ 高速な画像処理がPharoで可能に! ●librdkafka ○ Kafkaにつないで分散ストリーム処理 などなど
  • 29.
    まとめ ● Unified FFIを使うと既存のCの共有ライブラリを使ってPharo を拡張できる ●C <-> Smalltalkのマッピングが定義されており楽 ● コールアウト、コールインの両方がサポートされている いろいろなライブラリを呼び出してPharoを気軽に拡張してい きましょう!