Copyright  (C) 2021 Toranoana Inc. All Rights Reserved.
denoでFFIの続き



虎の穴ラボ 藤原 佳顕

1
Copyright  (C) 2021 Toranoana Inc. All Rights Reserved.
アジェンダ

● 概要
● 前回までのおさらい
● deno1.17で変わったこと
● 作ったもの
2
Copyright  (C) 2021 Toranoana Inc. All Rights Reserved.
自己紹介

3
● 名前

○ 藤原佳顕(@nuhera, @zonuko)

● 仕事

○ FantiaとかCreatiaとか社内アプリとか

● 好み

○ Clojure、Rust

● 趣味

○ 格闘ゲーム

■ Melty Blood、Guilty Gear

○ STG(ダライアスとか)も好き

○ 最近は女神転生Vずっとやってました

Copyright  (C) 2021 Toranoana Inc. All Rights Reserved.
概要

4
● (前回まで)

○ deno1.13あたりからFFI機能が追加されています


■ これによってDLLやSOファイルなど、いわゆるダイナミック/スタティックライブラリ
をDenoから呼び出せるように

● (今回について)

○ deno言語自体がアップデートされたことによって追加された機能について


○ 前回と変わった部分のソース紹介


Copyright  (C) 2021 Toranoana Inc. All Rights Reserved.
前回までのおさらい

5
● libmysqlclient→C(wrapper)→deno FFI


○ libmysqlclient.cをC言語でラップして、それをdeno FFIで実行することで実質denoから
libmysqlclient.cを呼べるようになった


● 課題だった点

○ 戻り値として文字列(*char)を使えなかった


■ deno側から見るとbuffer型だが引数にしか対応していなかった


■ したがってクエリの実行が実質できなかった


● できても結果を取れない

Copyright  (C) 2021 Toranoana Inc. All Rights Reserved.
前回までのおさらい(ソース)

● まずはコネクション取得(C側)

○ mysql_init→mysql_real_connectとする必要があるのでどちらのラッパー関数も定義(ついでにcloseも)

6
#include <mysql/mysql.h>
#include <stdio.h>
MYSQL *conn = NULL;
void init()
{
conn = mysql_init(NULL);
}
int connect(const char *host, const char *user, const char *passwd, const char *db_name, int port)
{
if (!mysql_real_connect(conn, host, user, passwd, db_name, port, NULL, 0)) {
fprintf(stderr, "Failed to connect to database: Error: %sn", mysql_error(conn));
fflush(stderr);
return 0;
}
return 1;
}
void close(void)
{
mysql_close(conn);
}
Copyright  (C) 2021 Toranoana Inc. All Rights Reserved.
前回までのおさらい(ソース)



● ネクション取得(deno側)

7
const C_TERMINAL_SYMBOL = "0";
const MYSQL_HOST = "localhost";
const MYSQL_USER = "root";
const MYSQL_PASSWD = "";
const MYSQL_DB_NAME = "mysql";
const MYSQL_PORT = 3306;
function strToBytes(word: string): Uint8Array {
return new TextEncoder().encode(`${word}${C_TERMINAL_SYMBOL}`);
}
function ffiSuffix(): string {
switch (Deno.build.os) {
case "windows":
return "dll";
case "darwin":
return "dylib";
case "linux":
return "so";
}
}
interface LibInterfaces extends Record<string, Deno.ForeignFunction> {
init: Deno.ForeignFunction;
connect: Deno.ForeignFunction;
close: Deno.ForeignFunction;
}
const libInterfaces: LibInterfaces = {
init: { parameters: [], result: "void" },
connect: {
parameters: ["buffer", "buffer", "buffer", "buffer", "i32"],
result: "i32",
},
close: { parameters: [], result: "void" },
};
Copyright  (C) 2021 Toranoana Inc. All Rights Reserved.
前回までのおさらい(ソース)

● コネクション取得続き(deno側)

8
class MySQL {
#dylib: Deno.DynamicLibrary<LibInterfaces> | null = null;
readonly host = strToBytes(MYSQL_HOST);
readonly user = strToBytes(MYSQL_USER);
readonly passwd = strToBytes(MYSQL_PASSWD);
readonly dbName = strToBytes(MYSQL_DB_NAME);
private isConn = false;
constructor() {
const libName = `./ext/mysql/mysql.${ffiSuffix()}`;
this.#dylib = Deno.dlopen(libName, libInterfaces);
this.#dylib.symbols.init();
const res = this.#dylib.symbols.connect(this.host, this.user, this.passwd, this.dbName, MYSQL_PORT);
if (res === 0) {
this.close();
throw new Error("コネクション確立に失敗しました。 ");
}
this.isConn = true;
}
close() {
if (!this.#dylib) {
return;
}
if (this.isConn) {
this.#dylib.symbols.close();
this.#dylib.close();
this.#dylib = null;
}
}
}
const client = new MySQL();
client.close();
Copyright  (C) 2021 Toranoana Inc. All Rights Reserved.
deno1.17で変わったこと

9
● buffer型がpointer型に変わりました


○ 実質的にポインタなら何でもやり取りできるようになりました


○ 型はDeno.UnsafePointerとDeno.UnsafePointerView


○ なんでもできる

■ 構造体のポインタ自体をやりとりできるのでCのラッパーが不要に


● sqliteのサンプル

○ https://deno.land/x/sqlite3@0.3.0


Copyright  (C) 2021 Toranoana Inc. All Rights Reserved.
作ったもの

10
const symbols = <Record<string, Deno.ForeignFunction>> {
mysql_init: {
parameters: [
"pointer", // MYSQL *mysql
],
result: "pointer",
},
mysql_real_connect: {
parameters: [
"pointer", // MYSQL *mysql
"pointer", // const char *host
"pointer", // const char *user
"pointer", // const char *passwd
"pointer", // const char *db
"u32", // unsigned int port
"pointer", // const char *unix_socket
"f32", // unsigned long client_flag
],
result: "pointer",
},
mysql_close: {
parameters: [
"pointer", // MYSQL *mysql
],
result: "void",
},
};
const libInterfaces: LibInterfaces = {
init: { parameters: [], result: "void" },
connect: {
parameters: ["buffer", "buffer", "buffer", "buffer", "i32"],
result: "i32",
},
close: { parameters: [], result: "void" },
};
↓前回までの同一箇所。bufferがpointerに変わっている

Copyright  (C) 2021 Toranoana Inc. All Rights Reserved.
作ったもの

11
const envMysqlPath = Deno.env.get("DENO_LIB_MYSQL_CLIENT_PATH");
if (envMysqlPath !== undefined) {
lib = Deno.dlopen(envMysqlPath, symbols);
} else {
try {
lib = Deno.dlopen(`libmysqlclient.${ffiSuffix()}`, symbols);
} catch (e) {
const error = new Error(
"libmysqlclient is not found.",
);
error.cause = e;
throw error;
}
}
FFI読み込み箇所


ほぼSQLiteのサンプルと同様


前回との違い

⇨Cのラッパーではなくlibmysqlclientを直接deno側から呼
び出すようになった


(↓前回のもの)

const libName = `./ext/mysql/mysql.${ffiSuffix()}`;
this.#dylib = Deno.dlopen(libName, libInterfaces);
Copyright  (C) 2021 Toranoana Inc. All Rights Reserved.
作ったもの

12
export type mysql = Deno.UnsafePointer;
export type ffi_string = string | null;
export function mysql_init(client: mysql | null): mysql {
return lib.symbols.mysql_init(client) as mysql;
}
mysql構造体を直にやり取りできるように(client変数)

実態は*MYSQLなのでpointerとすれば構造体ポインタな
らやり取りできる

libmysqlclientのインターフェースと全く同じものをdeno側
にも定義できる

this.#dylib.symbols.init(); 前回までは構造体のポインタはNG

なので、C言語内のグローバル変数としてラップしていた

Copyright  (C) 2021 Toranoana Inc. All Rights Reserved.
作ったもの

13
export function mysql_real_connect(
client: mysql,
host: ffi_string,
user: ffi_string,
passwd: ffi_string,
db: ffi_string,
port: number,
unix_socket: ffi_string,
client_flag: number,
): mysql {
const mysql_host = strToBytesWithTerminated(host);
const mysql_user = strToBytesWithTerminated(user);
const mysql_passwd = strToBytesWithTerminated(passwd);
const mysql_db = strToBytesWithTerminated(db);
const mysql_unix_socket = strToBytesWithTerminated(unix_socket);
return lib.symbols.mysql_real_connect(
client,
mysql_host,
mysql_user,
mysql_passwd,
mysql_db,
port,
mysql_unix_socket,
client_flag,
) as mysql;
}
export function mysql_close(client: mysql): void {
return lib.symbols.mysql_close(client) as void;
}
引数の文字列もpointer型に

Copyright  (C) 2021 Toranoana Inc. All Rights Reserved.
まとめ

● deno1.17でFFIのインターフェースとしてpointerが入りました


● pointer型を使うことでライブラリを直接denoで扱えるケースが増えた


○ 実質何でもできると思います

● サンプルとして前回まで作っていたlibmysqlclientを置き換えて見ました


● クエリの発行までできると思うのでやってみたいと思います


14

【20220120 toranoana.deno#4】denoでffiの続き

  • 1.
    Copyright  (C) 2021Toranoana Inc. All Rights Reserved. denoでFFIの続き
 
 虎の穴ラボ 藤原 佳顕
 1
  • 2.
    Copyright  (C) 2021Toranoana Inc. All Rights Reserved. アジェンダ
 ● 概要 ● 前回までのおさらい ● deno1.17で変わったこと ● 作ったもの 2
  • 3.
    Copyright  (C) 2021Toranoana Inc. All Rights Reserved. 自己紹介
 3 ● 名前
 ○ 藤原佳顕(@nuhera, @zonuko)
 ● 仕事
 ○ FantiaとかCreatiaとか社内アプリとか
 ● 好み
 ○ Clojure、Rust
 ● 趣味
 ○ 格闘ゲーム
 ■ Melty Blood、Guilty Gear
 ○ STG(ダライアスとか)も好き
 ○ 最近は女神転生Vずっとやってました

  • 4.
    Copyright  (C) 2021Toranoana Inc. All Rights Reserved. 概要
 4 ● (前回まで)
 ○ deno1.13あたりからFFI機能が追加されています 
 ■ これによってDLLやSOファイルなど、いわゆるダイナミック/スタティックライブラリ をDenoから呼び出せるように
 ● (今回について)
 ○ deno言語自体がアップデートされたことによって追加された機能について 
 ○ 前回と変わった部分のソース紹介 

  • 5.
    Copyright  (C) 2021Toranoana Inc. All Rights Reserved. 前回までのおさらい
 5 ● libmysqlclient→C(wrapper)→deno FFI 
 ○ libmysqlclient.cをC言語でラップして、それをdeno FFIで実行することで実質denoから libmysqlclient.cを呼べるようになった 
 ● 課題だった点
 ○ 戻り値として文字列(*char)を使えなかった 
 ■ deno側から見るとbuffer型だが引数にしか対応していなかった 
 ■ したがってクエリの実行が実質できなかった 
 ● できても結果を取れない

  • 6.
    Copyright  (C) 2021Toranoana Inc. All Rights Reserved. 前回までのおさらい(ソース)
 ● まずはコネクション取得(C側)
 ○ mysql_init→mysql_real_connectとする必要があるのでどちらのラッパー関数も定義(ついでにcloseも)
 6 #include <mysql/mysql.h> #include <stdio.h> MYSQL *conn = NULL; void init() { conn = mysql_init(NULL); } int connect(const char *host, const char *user, const char *passwd, const char *db_name, int port) { if (!mysql_real_connect(conn, host, user, passwd, db_name, port, NULL, 0)) { fprintf(stderr, "Failed to connect to database: Error: %sn", mysql_error(conn)); fflush(stderr); return 0; } return 1; } void close(void) { mysql_close(conn); }
  • 7.
    Copyright  (C) 2021Toranoana Inc. All Rights Reserved. 前回までのおさらい(ソース)
 
 ● ネクション取得(deno側)
 7 const C_TERMINAL_SYMBOL = "0"; const MYSQL_HOST = "localhost"; const MYSQL_USER = "root"; const MYSQL_PASSWD = ""; const MYSQL_DB_NAME = "mysql"; const MYSQL_PORT = 3306; function strToBytes(word: string): Uint8Array { return new TextEncoder().encode(`${word}${C_TERMINAL_SYMBOL}`); } function ffiSuffix(): string { switch (Deno.build.os) { case "windows": return "dll"; case "darwin": return "dylib"; case "linux": return "so"; } } interface LibInterfaces extends Record<string, Deno.ForeignFunction> { init: Deno.ForeignFunction; connect: Deno.ForeignFunction; close: Deno.ForeignFunction; } const libInterfaces: LibInterfaces = { init: { parameters: [], result: "void" }, connect: { parameters: ["buffer", "buffer", "buffer", "buffer", "i32"], result: "i32", }, close: { parameters: [], result: "void" }, };
  • 8.
    Copyright  (C) 2021Toranoana Inc. All Rights Reserved. 前回までのおさらい(ソース)
 ● コネクション取得続き(deno側)
 8 class MySQL { #dylib: Deno.DynamicLibrary<LibInterfaces> | null = null; readonly host = strToBytes(MYSQL_HOST); readonly user = strToBytes(MYSQL_USER); readonly passwd = strToBytes(MYSQL_PASSWD); readonly dbName = strToBytes(MYSQL_DB_NAME); private isConn = false; constructor() { const libName = `./ext/mysql/mysql.${ffiSuffix()}`; this.#dylib = Deno.dlopen(libName, libInterfaces); this.#dylib.symbols.init(); const res = this.#dylib.symbols.connect(this.host, this.user, this.passwd, this.dbName, MYSQL_PORT); if (res === 0) { this.close(); throw new Error("コネクション確立に失敗しました。 "); } this.isConn = true; } close() { if (!this.#dylib) { return; } if (this.isConn) { this.#dylib.symbols.close(); this.#dylib.close(); this.#dylib = null; } } } const client = new MySQL(); client.close();
  • 9.
    Copyright  (C) 2021Toranoana Inc. All Rights Reserved. deno1.17で変わったこと
 9 ● buffer型がpointer型に変わりました 
 ○ 実質的にポインタなら何でもやり取りできるようになりました 
 ○ 型はDeno.UnsafePointerとDeno.UnsafePointerView 
 ○ なんでもできる
 ■ 構造体のポインタ自体をやりとりできるのでCのラッパーが不要に 
 ● sqliteのサンプル
 ○ https://deno.land/x/sqlite3@0.3.0 

  • 10.
    Copyright  (C) 2021Toranoana Inc. All Rights Reserved. 作ったもの
 10 const symbols = <Record<string, Deno.ForeignFunction>> { mysql_init: { parameters: [ "pointer", // MYSQL *mysql ], result: "pointer", }, mysql_real_connect: { parameters: [ "pointer", // MYSQL *mysql "pointer", // const char *host "pointer", // const char *user "pointer", // const char *passwd "pointer", // const char *db "u32", // unsigned int port "pointer", // const char *unix_socket "f32", // unsigned long client_flag ], result: "pointer", }, mysql_close: { parameters: [ "pointer", // MYSQL *mysql ], result: "void", }, }; const libInterfaces: LibInterfaces = { init: { parameters: [], result: "void" }, connect: { parameters: ["buffer", "buffer", "buffer", "buffer", "i32"], result: "i32", }, close: { parameters: [], result: "void" }, }; ↓前回までの同一箇所。bufferがpointerに変わっている

  • 11.
    Copyright  (C) 2021Toranoana Inc. All Rights Reserved. 作ったもの
 11 const envMysqlPath = Deno.env.get("DENO_LIB_MYSQL_CLIENT_PATH"); if (envMysqlPath !== undefined) { lib = Deno.dlopen(envMysqlPath, symbols); } else { try { lib = Deno.dlopen(`libmysqlclient.${ffiSuffix()}`, symbols); } catch (e) { const error = new Error( "libmysqlclient is not found.", ); error.cause = e; throw error; } } FFI読み込み箇所 
 ほぼSQLiteのサンプルと同様 
 前回との違い
 ⇨Cのラッパーではなくlibmysqlclientを直接deno側から呼 び出すようになった 
 (↓前回のもの)
 const libName = `./ext/mysql/mysql.${ffiSuffix()}`; this.#dylib = Deno.dlopen(libName, libInterfaces);
  • 12.
    Copyright  (C) 2021Toranoana Inc. All Rights Reserved. 作ったもの
 12 export type mysql = Deno.UnsafePointer; export type ffi_string = string | null; export function mysql_init(client: mysql | null): mysql { return lib.symbols.mysql_init(client) as mysql; } mysql構造体を直にやり取りできるように(client変数)
 実態は*MYSQLなのでpointerとすれば構造体ポインタな らやり取りできる
 libmysqlclientのインターフェースと全く同じものをdeno側 にも定義できる
 this.#dylib.symbols.init(); 前回までは構造体のポインタはNG
 なので、C言語内のグローバル変数としてラップしていた

  • 13.
    Copyright  (C) 2021Toranoana Inc. All Rights Reserved. 作ったもの
 13 export function mysql_real_connect( client: mysql, host: ffi_string, user: ffi_string, passwd: ffi_string, db: ffi_string, port: number, unix_socket: ffi_string, client_flag: number, ): mysql { const mysql_host = strToBytesWithTerminated(host); const mysql_user = strToBytesWithTerminated(user); const mysql_passwd = strToBytesWithTerminated(passwd); const mysql_db = strToBytesWithTerminated(db); const mysql_unix_socket = strToBytesWithTerminated(unix_socket); return lib.symbols.mysql_real_connect( client, mysql_host, mysql_user, mysql_passwd, mysql_db, port, mysql_unix_socket, client_flag, ) as mysql; } export function mysql_close(client: mysql): void { return lib.symbols.mysql_close(client) as void; } 引数の文字列もpointer型に

  • 14.
    Copyright  (C) 2021Toranoana Inc. All Rights Reserved. まとめ
 ● deno1.17でFFIのインターフェースとしてpointerが入りました 
 ● pointer型を使うことでライブラリを直接denoで扱えるケースが増えた 
 ○ 実質何でもできると思います
 ● サンプルとして前回まで作っていたlibmysqlclientを置き換えて見ました 
 ● クエリの発行までできると思うのでやってみたいと思います 
 14