Android上のRustでSQLiteを扱おうとしたときに困った事

Android上のRustからSQLiteを扱おうとしたときに、ライブラリの都合上rusqliteを使用した。 で、Connection::open_in_memory()は動いたのだが、ファイルに保存しようとしてConnection::open("dummy")のような事をしたらエラーにすらならずopenが返ってこなくなった。

色々いじってみたところ、結局dart側の getApplicationSupportDirectory() でディレクトリの絶対パスを取得し、それをくっ付けたパスにしたところ動作した。 絶対パスを指定しないで変な場所にアクセスしようとしたのはまあこちらが悪いのだが、せめて権限エラーか何かになって欲しかった。。。

openssl-sysのビルドエラー

Android用にRustで書いていて遭遇したけど対処法がパッと分からなかったもののメモ

error: failed to run custom build command for `openssl-sys v0.9.58`

おそらくWebサーバへのアクセスのためにreqwestを使っていて、その依存と思われる。 ubuntu x86_64だと特に問題なくビルドできる。

結局対象法としては Cargo.toml の [depenenceis] に、

openssl = { version = "0.10", features = ["vendored"] }

と書いたら動いた。

Claspをビルドする2020-05

Common Lisp処理系のClaspは、依存ライブラリやリビジョンが色々変わって中々出来ない事が多いので、現時点でビルド可能な組み合わせについて。

ホスト計算機はUbuntu 18.04.4 x86_64 で確認を行っている。 Ubuntuのパッケージ方針がよく分からないが、18.04でも時期によってLLVMのバージョンが色々あったりするので、例えばapt install clangとやると、Claspをビルドできないclangが入る可能性がある。 改めてUbuntu 18.04のLLVM関連パッケージをみると、現在では3.9から9までそろっているのでUbuntu 18.04だけを考えればexternals claspでLLVMをビルドする必要は無いかもしれない。

testingブランチ

ブランチの扱いが時々変わるのでよく分からないところがあるが、一応安定版に相当すると思われるブランチ。 現在の最新バージョンは2019-09-29の f0cb8960867be6ed58a82066abb0da2df9da72b3 でこれはgithub上 0.4.3 のタグがついている最新安定版と思われる。 LLVM 6を必要としており、現状のroswellからインストールできるのはこのブランチを追従してることが多い。

roswellからのインストール

完全な手順を目指して、dockerのubuntu:bionicでインストールを行う。

docker pull ubuntu:bionic
docker run -it --name roswell-clasp ubuntu:bionic

dockerのubuntuの中でroswellインストール。

apt update
apt -y install git build-essential automake libcurl4-openssl-dev
git clone -b release https://github.com/roswell/roswell.git
cd roswell
git checkout v20.04.14.105
sh bootstrap
./configure --prefix=$HOME/.local
make
make install
export PATH=$HOME/.local/bin:$PATH
ros setup
ros run -- --version

最終的にroswellが動いて、インストールされたSBCLは2.0.4だった。

SBCL 2.0.4

Claspをインストール。まずは依存ライブラリのビルド。

apt -y install cmake python binutils-dev
time ros install externals-clasp+/6.0.1

数十分待つとビルドが終わる。エラーになるようであれば、~/.roswell/lib/x86-64/linux/externals_clasp/6.0.1の中で make を実行してエラーに対処する。(環境合わせてこの手順通りならエラーにならないはずだが)

apt -y install libgmp-dev libgc-dev zlib1g-dev libelf-dev libncurses-dev libbsd-dev libboost-filesystem-dev libboost-date-time-dev libboost-program-options-dev libboost-iostreams-dev
time ros install clasp  

数十分待つとビルドが終わる。エラーになるようであれば、 ~/.roswell/src/clasp/2019-09-29 の中で ./waf configure build_dboehm を実行してエラーに対処する。(環境合わせてこの手順通りならエラーにならないはずだが)

ros run -- --version
clasp-boehm-0.4.2-1534-gf0cb89608

普通にビルド

公式ビルド方法でもそのままビルドできる。 Build Instructions · clasp-developers/clasp Wiki · GitHub

docker run -it --name clasp-testing ubuntu:bionic
apt update
apt install -y gcc g++ llvm clang-6.0 libclang-6.0-dev cmake libgc-dev libgmp-dev binutils-gold binutils-dev zlib1g-dev libncurses-dev libboost-filesystem-dev libboost-regex-dev libboost-date-time-dev libboost-program-options-dev libboost-system-dev libboost-iostreams-dev libunwind-dev liblzma-dev libelf1 libelf-dev libbsd-dev sbcl git
git clone https://github.com/clasp-developers/clasp.git
cd clasp
git checkout f0cb8960867be6ed58a82066abb0da2df9da72b3
time ./waf configure build_dboehm

数十分待つとビルドが終わる。

build/clasp 

実行するとREPLが起動する。

Starting cclasp-boehm-0.4.2-1534-gf0cb89608-cst ... loading image...
Top level in: #<PROCESS TOP-LEVEL @0x7900ea9>.
COMMON-LISP-USER> 

devブランチ

devブランチは最新開発版で現在の dee626ca92ce5b2154c1903b17d26444a7f69de6LLVM 9を必要としている。

docker run -it --name clasp-dev ubuntu:bionic
apt update
apt install -y git sbcl clang-9 libclang-9-dev libgmp-dev libgc-dev zlib1g-dev libelf-dev libncurses-dev libbsd-dev libboost-filesystem-dev libboost-date-time-dev libboost-program-options-dev libboost-iostreams-dev
git clone https://github.com/clasp-developers/clasp.git
cd clasp
git checkout dee626ca92ce5b2154c1903b17d26444a7f69de6
echo BOEHM_GC_ENUMERATE_REACHABLE_OBJECTS_INNER_AVAILABLE=False > wscript.config
time ./waf configure build_dboehm

数十分待つとビルドが終わる。

build/clasp

実行するとREPLが起動する。

Starting cclasp-boehm-0.4.2-2436-gdee626ca9-cst ... loading image...
Top level in: #<PROCESS TOP-LEVEL @0x9adbb69 (Running)>.
COMMON-LISP-USER> 

その他

  • ClaspはCMakeとWafとMakefileが混在しているのでよく分からんが、現状ビルドのメインはWaf部分だと思われる。
  • externals claspをroswellで使うようになったのも、当時ubuntuのパッケージにはLLVM 6がclaspに必要だったためだった気がする。

Android版のFlutterからRustを呼び出す

Android版のFlutterからRustの関数を呼び出す最低限のメモ。 それらしい記事はいくつかあるものの、理解が出来てないのかバージョンの違いからか中々上手く行かなかったので。 Fluttter, Android, Rust等々の作法として正しいかどうかは正直分からないし、少し前まではDartから直接FFIが扱えなかったのでJNI経由だったようだ。

以下参考にしたページ。

環境

  • Zephyrus GX502
  • Zenfone 6

ツールバージョン

  • Android
    • Studio 3.6.2
    • SDK 10.0 29 4
    • Emulator 30.0.5
    • SDK Platform Tools 29.0.6
    • SDK Tools 26.1.1
  • Dart 192.7761
  • Flutter 45.1.1
  • Rust 1.41.0
  • rustup 1.21.1

インストール

Flutter

普通にインストール。

  1. Android Studio
  2. Flutter
  3. Android NDK
  4. Install and configure the NDK and CMake  |  Android Developers
    • SDK Manager -> SDK Tools -> NDKインストール

Rust

rustupをインストールしrustのandroid関連パッケージをインストールする。

curl https://sh.rustup.rs -sSf | sh
rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android
cargo install cargo-ndk

新規Flutterプロジェクト作成

  1. Start a New Flutter project選択
  2. Flutter Application選択
  3. New Flutter Application
    • Project name : flutter_dart_ffi_rust
    • Flutter SDK path : /home/username/.local/flutter
    • Project location : /home/username/AndroidStudioProjects/flutter_dart_ffi_rust
    • Description : Flutter Dart FFI Rust
    • Package name : com.example.flutterdartffirust
    • AndroidX : on
    • Platform channel language
      • Include Kotlin suppport for Android code : on
      • Include Swift support for iOS code : off

新規Rustライブラリ作成

Flutterプロジェクトの中ににRustのライブラリを作成する。

cd flutter_dart_ffi_rust && mkdir rust && cd rust
cargo init --name rust_sample --lib

src/lib.rsを書き換えてC ABIの関数echoを作る。

use std::ffi::{CStr, CString};
use std::os::raw::c_char;

#[no_mangle]
pub unsafe extern "C" fn echo(to: *const c_char) -> *mut c_char {
    let c_str = CStr::from_ptr(to);
    let s = match c_str.to_str() {
        Ok(s) => s,
        Err(_) => "Err",
    };

    CString::new(format!("From Rust {}", s)).unwrap().into_raw()
}

rust/build.sh ビルドスクリプト。 ローカルのシミュレータやAndroid実機等で動かすため複数のプロセッサ向けにコンパイルする。

export ANDROID_NDK_HOME=$HOME/Android/Sdk/ndk/21.0.6113669

for target in aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android
do
  cargo ndk --target ${target} --android-platform 22 -- build --release
done

rust/install.sh インストールスクリプト。 最終的な実行時に、共有ライブラリが参照できる場所は具体的にどこなのか作法としてよく分からないが、JNIと同様Flutterの android/app/src/main/jniLibs/ 以下に置いて動いた。

INSTALL_PATH=../android/app/src/main/jniLibs
INSTALL_FILE=librust_sample.so

mkdir -p ${INSTALL_PATH}/arm64-v8a
mkdir -p ${INSTALL_PATH}/armeabi-v7a
mkdir -p ${INSTALL_PATH}/x86
mkdir -p ${INSTALL_PATH}/x86_64

cp target/aarch64-linux-android/release/${INSTALL_FILE} ${INSTALL_PATH}/arm64-v8a/
cp target/armv7-linux-androideabi/release/${INSTALL_FILE} ${INSTALL_PATH}/armeabi-v7a/
cp target/i686-linux-android/release/${INSTALL_FILE} ${INSTALL_PATH}/x86/
cp target/x86_64-linux-android/release/${INSTALL_FILE} ${INSTALL_PATH}/x86_64/

Flutter側修正

pubspec.yamldependencies:ffiを追加する。

ffi: ^0.1.3

lib/main.dart からRustのecho関数を呼び出す。 ffiのインポートを追加し、dll読み込み、呼び出す関数の型定義、echo関数の生成(?)を書く。

import 'dart:ffi';
import 'package:flutter/material.dart';
import 'package:ffi/ffi.dart';

final dll = DynamicLibrary.open('librust_sample.so');
typedef Echo = Pointer<Utf8> Function(Pointer<Utf8> to);
final echo = dll.lookup<NativeFunction<Echo>>('echo').asFunction<Echo>();

void main() => runApp(MyApp());
// 略

lib/main.dart'Flutter Demo Home Page' を関数呼び出しで置き換える。

// 略
home: MyHomePage(title: Utf8.fromUtf8(echo(Utf8.toUtf8('dummy')))),
// 略

実行

Zenfone6上で実行出来た。

f:id:gos-k:20200426180019j:plain

ユニットテストでダミーの関数に差し替える

Common Lispユニットテストを行う場合に、ユニットテストから呼び出した関数の内部で呼ばれる関数を本来の動作とは違う別の関数に差し替えたい場合がある。

関数を一時的に置き換える方法についてのメモ。

関数

例えば次の例では関数 charlie をテストしようと思った場合に、その中で呼び出される関数 alfa を差し替えたいとする。

(defun alfa ()
  :bravo)

(defun charlie ()
  (alfa))

(pprint (eq (charlie) :delta))

上記のコードは、charlie:bravo を返すため nil となる。

危険なバージョン

最低限の機能を示すための危険なバージョンが次の通り。

(defun alfa ()
  :bravo)

(defun charlie ()
  (alfa))

(setf (symbol-function 'alfa) (lambda () :delta))
(pprint (eq (charlie) :delta))

上記のコードは、charlie:delta を返すため t となる。 最後から2行目で symbol-function への setf で、関数 alfacharlie 自体の変更をすることなく呼び出される関数 alfa を変更する事が出来ている。

多分安全なバージョン

ユニットテストで一時的に関数を変更したいということを考えると、最後は元々の関数に戻って欲しい。そのバージョンが次の通り。

(defun alfa ()
  :bravo)

(defun charlie ()
  (alfa))

(let ((orig-func (symbol-function 'alfa)))
  (unwind-protect (progn
                    (setf (symbol-function 'alfa) (lambda () :delta))
                    (pprint (eq (charlie) :delta)))
    (setf (symbol-function 'alfa) orig-func)))
(pprint (eq (charlie) :delta))

最後から3行目のコードは関数 alfa が差し変わっているので t となり、最後の行は関数 alfa が元に戻っているので nil となる。 unwind-protect は内部でなんらかコンディションが発生した場合でも確実に元に戻せるように。

メソッド

(defclass delta () ())

(defmethod echo :before ((delta delta))
  (pprint :foxtrot))

(defmethod echo ((delta delta))
  (pprint :golf))

(echo (make-instance 'delta))

実行結果

:FOXTROT
:GOLF

クラスのメソッドを差し替える場合には、総称関数およびオブジェクトシステムに関連して一手間かける必要がある。 まず総称関数オブジェクトを取得し、それを使ってオブジェクトシステムからqualifierやspecifierの一致するメソッドを取得する。

一度beforeメソッドを削除して、元に戻す。

(defclass delta () ())

(defmethod echo :before ((d delta))
  (pprint :foxtrot))

(defmethod echo ((d delta))
  (pprint :golf))

(let ((d (make-instance 'delta))
       (orig-method (find-method #'echo  '(:before) (mapcar #'find-class '(delta)))))
  (remove-method #'echo orig-method)
  (echo d)
  (add-method #'echo orig-method)
  (echo d))

実行結果

:GOLF
:FOXTROT
:GOLF

Carpをチョット触るその4

Carpをチョット触るその3 - gos-k’s blog

文法とか

https://github.com/carp-lang/Carp/blob/master/docs/LanguageGuide.md を見ながら文法の確認。

特殊形式は fn let do if while ref address set! the の9個だが(これで全部?)流石にそのままでは不便で、core/Macros.carpに色々ある。

例えば条件分岐だと cond when unless case があって、本体に一つの式しか取れないので when-do も定義されている。(unless-doはないのか?)

反復だと for foreach while-do forever-do

fib

たいして変わらないけど、一応フィボナッチ数もやってみる。

(defn fib [x]
  (cond
    (= x 0) 0
    (= x 1) 1
    (+ (fib (- x 1)) (fib (- x 2)))))

(defn main []
  (print* (fib 40) @"\n"))
time carp -x fib.carp
102334155

real    0m21.038s
user    0m17.416s
sys 0m0.480s

比較対象としてCommon Lisp版。処理系はSBCL 1.5.6

(defun fib (x)
  (cond
    ((= x 0) 0)
    ((= x 1) 1)
    (t (+ (fib (1- x)) (fib (- x 2))))))

(defun main ()
  (pprint (fib 40)))
time ros run -l fib.lisp -e "(main)" -q
102334155
real    0m8.985s
user    0m7.405s
sys 0m0.109s

2倍位Carpが遅いがまあこんなもんなのかな? Carpの出力コードのfib関数はこんな感じ。

int fib(int x) {
    int _39;
    bool _8 = Int__EQ_(x, 0);
    if (_8) {
        int _11 = 0;
        _39 = _11;
    } else {
        int _37;
        bool _17 = Int__EQ_(x, 1);
        if (_17) {
            int _20 = 1;
            _37 = _20;
        } else {
            Lambda _23 = { .callback = fib, .env = NULL, .delete = NULL, .copy = NULL }; //Sym fib LookupRecursive
            int _27 = Int__MINUS_(x, 1);
            int _28 = _23.env ? ((int(*)(LambdaEnv, int))_23.callback)(_23.env, _27) : ((int(*)(int))_23.callback)(_27);
            Lambda _29 = { .callback = fib, .env = NULL, .delete = NULL, .copy = NULL }; //Sym fib LookupRecursive
            int _33 = Int__MINUS_(x, 2);
            int _34 = _29.env ? ((int(*)(LambdaEnv, int))_29.callback)(_29.env, _33) : ((int(*)(int))_29.callback)(_33);
            int _35 = Int__PLUS_(_28, _34);
            int _36 = _35;
            _37 = _36;
        }
        int _38 = _37;
        _39 = _38;
    }
    return _39;
}

再帰呼び出しでちょっと遠回りしてるが、まだまだシンプルで素直に読める。

CFFI

出力がCなのでCFFIもサポート。 CarpからCのヘッダファイルを読み込む。

  • system-include
  • local-include

Carp上での型定義。

  • register
  • register-type

これやってて混乱するのはString型に感することで、Carpの String はCの char * に対応する。( const char * か?) それだけならいいが、Carpの文字列リテラル&String でありCの char * * に対応する。 なので例えば、

(system-include "stdio.h")
(register puts (Fn [String] Int))

(defn main []
  (puts "Hello, world!"))

と書きたくなるんだけど、Carpのputsに渡しているのが文字列リテラルで型が &String なのに対して、Cのputsは char * でCarpの String だから素直にいかない。 この辺について見てると、core/IO.carpcore/carp_io.h でprintln関数について、

void IO_println(String *s) { puts(*s); }

とやってCarpの &String で受け取って、Cの関数でデリファレンスするという回りくどいことをするもの、らしい。 Carp側だと str で型を合わせられる。

(system-include "stdio.h")
(register puts (Fn [String] Int))

(defn main []
  (puts (str "Hello, world!")))
carp -x cffi.carp
Hello, world!
[RUNTIME ERROR] '"./out/Untitled"' exited with return value 14.

putsの返り値がそのままmainに返ってエラーがでてるけど、一応動作はする。