RustをNimから呼び出す
モチベーション
NimはPythonのような簡単な文法で、C言語にトランスパイルしバイナリにコンパイルすることで、学習コストの低さ、開発の生産性の高さ、実行速度の速さを兼ね揃えたプログラミング言語です。
所有権・借用に基づくスコープベースでの安全なメモリ管理をコンパイラが自動で行い、参照やポインタについて考える必要もないため、特にアプリケーション開発において「コーディングのためのコーディング」を減らせてその記述をビジネスロジックだけに集中させることができます。
しかしまだ普及しているとは言い難く、使わない人の意見を聞くと「ライブラリが少ない」という理由を多く聞きます。
Nimはコンパイル時に一旦C言語に変換するために、既にC言語で存在している資産を簡単に取り込むことができ、動的リンク・静的アーカイブ両方と非常にシームレスに連携することができます。
一方でRustは低レイヤーにおけるメモリ安全を徹底したプログラミング言語で、セグフォやメモリリークを防ぎ非常に高速に動作します。
しかし変数の所有権や借用を開発時に考慮する必要があり、とても学習コストが高いです。少なくとも文系出身2年目PHPerが簡単に扱えるような言語ではありません。
この2つの言語の特徴を考えた時に、数学に基づくアルゴリズムの実装などのライブラリはRustで、アプリケーションはNimで作ると両者のいいとこ取りができるのではないでしょうか。
Nim、Rust両方共にC言語を介したFFIの機構が備わっているため、今回はそれを使ってRustで作ったライブラリをNimのアプリケーションから呼ぶ実験をします。
環境構築
NimとRust両方の環境が入ったDockerコンテナを作ります。
FROM ubuntu:22.04
# prevent timezone dialogue
ENV DEBIAN_FRONTEND=noninteractive
RUN apt update --fix-missing && \
apt upgrade -y
RUN apt install -y --fix-missing \
gcc \
xz-utils \
ca-certificates \
curl \
pkg-config
WORKDIR /root
# ==================== Nim ====================
RUN curl https://nim-lang.org/choosenim/init.sh -sSf | sh -s -- -y
ENV PATH $PATH:/root/.nimble/bin
# ==================== Rust ====================
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y
WORKDIR /application
プロジェクト作成
/application
配下にsrc
ディレクトリを作りそこで作業します。
Nimでプロジェクトを作る。
cd /application/src
nimble init nimapp
対話型で聞かれるので、選択肢をTabでサイクルしてEnterで選びます。
Package type?
ではBinary
を選びます。
Info: Package initialisation requires info which could not be inferred.
... Default values are shown in square brackets, press
... enter to use them.
Using "nimapp" for new package name
Prompt: Your name? [Anonymous]
Answer: Using "src" for new package source directory
Prompt: Package type?
... Library - provides functionality for other packages.
... Binary - produces an executable for the end-user.
... Hybrid - combination of library and binary
... For more information see https://goo.gl/cm2RX5
Select Cycle with 'Tab', 'Enter' when done
Answer: binary
Prompt: Initial version of package? [0.1.0]
Answer: Prompt: Package description? [A new awesome nimble package]
Answer: Prompt: Package License?
... This should ideally be a valid SPDX identifier. See https://spdx.org/licenses/.
Select Cycle with 'Tab', 'Enter' when done
Answer: MIT
Prompt: Lowest supported Nim version? [1.6.10]
Answer: Success: Package nimapp created successfully
Rustでプロジェクトを作る。
cd /application/src
cargo new rustlib --lib
こういうディレクトリ構造になります。
/application
`-- src
|-- nimapp
| |-- nimapp.nimble
| |-- src
| | `-- nimapp.nim
| `-- tests
| |-- config.nims
| `-- test1.nim
`-- rustlib
|-- Cargo.toml
`-- src
`-- lib.rs
関数を呼ぶ
まずは簡単な、intを足し算するadd関数を作ってみましょう。
Rust側
#[no_mangle]
pub extern "C" fn add(a: i64, b: i64) -> i64 {
return a + b;
}
#[no_mangle]
これを関数に対して付けることで、C/Nimから、Rustで定義した通りのadd
の関数名で呼び出せるようになります。
pub extern "C"
関数に対してこれを付けることで、C/Nimから呼び出せる関数になります。
ライブラリを出力する時には、crate-type
を設定します。
動的ライブラリにコンパイルする時にはcdylib
を、静的アーカイブにコンパイルする時にはstaticlib
にします。
[package]
name = "rustlib"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
name = "rustlib"
crate-type = ["cdylib"]
# crate-type = ["staticlib"]
コンパイルします。
cd /application/src/rustlib
cargo build --release
/application/src/rustlib/target/release/librustlib.so
にShard Objectファイルが出力されました。これをNimから呼び出して使います。
Nim側
/application/src/nimapp/src/rustlib.nim
というファイルを作り、Shard Objectにある関数をNimから呼べるようにするためのグルー関数を定義します。
const libpath = "/application/src/rustlib/target/release/librustlib.so"
proc add*(a, b:int64):int64 {.dynlib:libpath, importc: "add".}
静的アーカイブを呼び出す時にはこのようにします。
const libpath = "/application/src/rustlib/target/release/librustlib.a"
{.passL:libpath.}
proc add*(a, b:int64):int64 {.cdecl, importc: "add".}
あとはnimapp.nim
からこのadd
関数を呼び出せばいいだけです。
import std/unittest
import ./rustlib
suite "test":
test "add":
echo add(1, 2)
check add(1, 2) == 3
実行しましょう。
cd /application/src/nimapp
nim c -r -f --mm:orc src/nimapp
[Suite] test
3
[OK] add
呼び出すことができました。
動的配列を扱う
RustのVectorをNimで扱うにはどうすればいいでしょうか。
ここではフィボナッチ数列を返す関数を使って説明します。
Rust側
フィボナッチ数を返す関数、それを内部で呼んでフィボナッチ数列を返す関数を定義します。
fn fib(n: u64) -> u64 {
match n {
0 => 0,
1 => 1,
_ => fib(n - 2) + fib(n - 1),
}
}
#[no_mangle]
pub extern "C" fn fib_array(n: u64) -> *mut Vec<u64> {
let mut vector = Vec::with_capacity(n.try_into().unwrap());
for i in 0..n {
vector.push(fib(i));
}
Box::into_raw(Box::new(vector))
}
#[no_mangle]
pub extern "C" fn get_fib_len(v: &mut Vec<u64>) -> usize {
v.len()
}
#[no_mangle]
pub extern "C" fn get_fib_item(v: &mut Vec<u64>, offset: usize) -> u64 {
v[offset]
}
fib_arrayの返り値の型は*mut Vec<u64>
にし、関数の最後でBox::into_raw(Box::new(vector))
を呼んでヒープの生ポインタにして返します。
更にVectorから長さとオフセット位置の値を返す関数も実装します。
Nim側
type FibPtr = ptr object
proc fibArrayLib(n:uint64):FibPtr {.dynlib:libpath, importc: "fib_array".}
proc len(self:FibPtr):int {.dynlib:libpath, importc: "get_fib_len".}
proc `[]`(self:FibPtr, offset:int):int {.dynlib:libpath, importc: "get_fib_item".}
proc fibArray*(n:int):seq[int] =
let v = fibArrayLib(n.uint64)
defer: v.dealloc()
var s = newSeq[int](n)
for i in 0..<v.len:
s[i] = v[i]
return s
Rustのget_fib_len
の返り値はヒープの生ポインタなので、それをマッピングするための独自のオブジェクトをFibPtr
として定義します。
Nimの関数は全て静的な型チェックとオーバーロードされて動くので、ここで定義した関数は全てFibPtr
の型を持つオブジェクトに対してのみ動きます。
fibArray
関数の中でRust側で定義した関数たちを呼び、生ポインタからVectorの長さとオフセット位置の値を取得し、Nimの動的配列であるSeq(Sequence)に詰め替えて返しています。
Nimでは生ポインタはNimのメモリ管理の管轄外になります。ポインタのメモリを開放するdealloc
関数が用意されているので、defer
を使ってスコープを抜けるとメモリが開放されるようにします。
このdefer
はGo言語と同じです。
ではnimapp
の中で呼び出してみましょう。
import std/unittest
import ./rustlib
suite "test":
test "add":
echo add(1, 2)
check add(1, 2) == 3
test "fib array":
let res = fibArray(10)
echo res
check res == @[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
cd /application/src/nimapp
nim c -r -f --mm:orc src/nimapp
[Suite] test
3
[OK] add
@[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
[OK] fib array
呼び出すことができました。
カスタム型(独自型、構造体)を扱う
Rustの中で定義した構造体のインスタンスをNimから扱えるようにします。
Rust側
submods/person.rs
というファイルを作ります。
数値と文字列のフィールドを持つPerson
型と、そのコンストラクタ、ゲッターメソッドを定義します。
FFIに出力する関数名はなるべく被らないような命名にした方がいいでしょう。そのためただidを返すメソッド名もid
ではなくget_person_id
としています。
mod submods {
pub mod fib;
+ pub mod c_ffi;
+ pub mod person;
}
+ use crate::submods::c_ffi;
#[no_mangle]
pub extern "C" fn add(a: i64, b: i64) -> i64 {
return a + b;
}
use std::ffi::c_char;
use crate::c_ffi;
pub struct Person {
id: i64,
name: String,
}
impl Person {
pub fn new(id: i64, name: String) -> Box<Person> {
let person = Box::new(Person { id, name });
person
}
pub fn id(&self) -> i64 {
self.id
}
pub fn name(&self) -> String {
self.name.to_string()
}
}
// ==================== FFI ====================
#[no_mangle]
pub extern "C" fn new_person(id: i64, _name: *const c_char) -> *mut Person {
let name = c_ffi::cstirng_to_string(_name);
let person = Person::new(id, name);
Box::into_raw(person)
}
#[no_mangle]
pub extern "C" fn get_person_id(person: &Person) -> i64 {
person.id()
}
#[no_mangle]
pub extern "C" fn get_person_name(person: &Person) -> *mut c_char {
c_ffi::string_to_cstring(person.name())
}
// ==================== test ====================
#[cfg(test)]
mod person_tests {
use super::*;
#[test]
fn person_test() {
let person = Person::new(1, "John".to_string());
assert_eq!(person.id(), 1);
assert_eq!(person.name(), "John");
}
}
new_person
関数の引数nameの型は*const c_char
になっています。これはC言語の文字列をRustで扱うための型です。
反対にRust→C言語へ文字列を返すには*mut c_char
にします。
new_person
関数の返り値の型は*mut Person
です。これは先ほどのフィボナッチ数列と同じく、ヒープの生ポインタになっています。
Nimの文字列もRustの文字列もそれぞれの言語の実行環境の中でみ動作する独自の型です。
そのためC言語を介してNimからRustへ文字列やりとりするためには、相互に変換する必要があります。
ここではまずRust側にC言語の文字列を相互変換する関数を作りました。
use std::ffi::c_char;
use std::ffi::CStr;
use std::ffi::CString;
pub fn cstirng_to_string(_arg: *const c_char) -> String {
let arg = unsafe {
assert!(!_arg.is_null());
let c_str = CStr::from_ptr(_arg);
let str_slice = c_str.to_str().unwrap();
drop(c_str);
str_slice.to_owned()
};
arg
}
pub fn string_to_cstring(_arg: String) -> *mut c_char {
CString::new(_arg).unwrap().into_raw()
}
person.rs
ではこれを呼び出しています。
Nim側
type
PersonObj {.pure, final.} = object
id:int
name:cstring
PersonPtr = ptr PersonObj
Person* = ref object
rawPtr: PersonPtr
proc newPerson(id:int, name:cstring):PersonPtr {.dynlib:libpath, importc:"new_person".}
proc new*(_:type Person, id:int, name:string):Person = Person(rawPtr:newPerson(id, name.cstring))
proc getPersonId(self:PersonPtr):int64 {.dynlib:libpath, importc:"get_person_id".}
proc id*(self:Person):int = self.rawPtr.getPersonId().int
proc getPersonName(self:PersonPtr):cstring {.dynlib:libpath, importc:"get_person_name".}
proc name*(self:Person):string = $self.rawPtr.getPersonName()
Rustの構造体定義と同じ構造体をNimのobjectで定義します。
実際にRustの関数とやりとりするのはヒープの生ポインタなので、マッピングするためのポインタオブジェクトPersonPtr
を定義します。
ポインタはNimのメモリ管理の管轄外になりますが、ポインタの型をフィールドに持つref
のオブジェクトは自動でメモリ管理されるので、Person* = ref object
を定義しNimからはこちらを扱うようにします。これによりdealloc
使って明示的にメモリ解放をする必要がなくなります。
newPerson
の引数nameの型はcstring
です。これがNimの中でのC言語の文字列に相当し、"文字列".cstring
とすれば型変換することができます。
name
関数の中ではgetPersonName
を呼び出していますが、getPersonName
の返り値の型はcstring
なので、$
を付けてstring
に変換しています。$
はNimの世界ではあらゆる型を文字列に変換するマジックメソッドです。(実際には全ての型に$
という同名の関数名で文字列に変換するように実装されている)
proc new*(_:type Person, id:int, name:string):Person = Person(rawPtr:newPerson(id, name.cstring))
proc id*(self:Person):int = self.rawPtr.getPersonId().int
proc name*(self:Person):string = $self.rawPtr.getPersonName()
この3つの関数はNimのアプリケーションから呼び出されRustの関数とマッピングしたnewPerson
などの関数を呼び出す処理と型変換を行うグルーコードです。
では呼び出してみましょう。
import std/unittest
import ./rustlib
suite "object":
test "person":
let person = Person.new(1, "John")
echo person.repr
echo person.id()
echo person.name()
check:
person.id() == 1
person.name() == "John"
[Suite] object
Person(rawPtr: PersonPtr(id: 1, name: "John"))
1
John
[OK] person
PersonPtr
オブジェクトのフィールドへの値のマッピングも、関数呼びだしも上手く行っています。
セッターを持つ独自型を扱う
これまでインスタンス生成とゲッターメソッドしか扱っていませんが、セッターメソッドでも上手くいくでしょうか
フィールドを更新することができるUpdatablePerson
型を使って説明します。
Rust側
mod submods {
pub mod fib;
pub mod c_ffi;
pub mod person;
+ pub mod updatable_person;
}
use crate::submods::c_ffi;
#[no_mangle]
pub extern "C" fn add(a: i64, b: i64) -> i64 {
return a + b;
}
use std::ffi::c_char;
use crate::submods::c_ffi;
pub struct UpdatablePerson {
id: i64,
name: String,
}
impl UpdatablePerson {
pub fn new(id: i64, name: String) -> Box<UpdatablePerson> {
let person = Box::new(UpdatablePerson { id, name });
person
}
pub fn id(&self) -> i64 {
self.id
}
pub fn set_id(&mut self, id: i64) {
self.id = id
}
pub fn name(&self) -> String {
self.name.to_string()
}
pub fn set_name(&mut self, name: String) {
self.name = name
}
}
#[no_mangle]
pub extern "C" fn new_updatable_person(id: i64, _name: *const c_char) -> *mut UpdatablePerson {
let name = c_ffi::cstirng_to_string(_name);
let person = UpdatablePerson::new(id, name);
Box::into_raw(person)
}
#[no_mangle]
pub extern "C" fn get_updatable_person_id(person: &UpdatablePerson) -> i64 {
person.id()
}
#[no_mangle]
pub extern "C" fn set_updatable_person_id(person: &mut UpdatablePerson, id: i64) {
person.set_id(id)
}
#[no_mangle]
pub extern "C" fn get_updatable_person_name(person: &UpdatablePerson) -> *mut c_char {
c_ffi::string_to_cstring(person.name())
}
#[no_mangle]
pub extern "C" fn set_updatable_person_name(person: &mut UpdatablePerson, _name: *const c_char) {
let name = c_ffi::cstirng_to_string(_name);
person.set_name(name)
}
#[cfg(test)]
mod updatable_person_test {
use super::*;
#[test]
fn test1() {
let mut person = UpdatablePerson::new(1, "John".to_string());
assert_eq!(person.id(), 1);
assert_eq!(person.name(), "John");
person.set_id(2);
person.set_name("Paul".to_string());
assert_eq!(person.id(), 2);
assert_eq!(person.name(), "Paul");
}
}
Nim側
type
UpdatablePersonObj {.pure, final.} = object
id:int
name:cstring
UpdatablePersonPtr = ptr UpdatablePersonObj
UpdatablePerson* = ref object
rawPtr: UpdatablePersonPtr
proc newUpdatablePerson(id:int, name:cstring):UpdatablePersonPtr {.dynlib:libpath, importc:"new_updatable_person".}
proc new*(_:type UpdatablePerson, id:int, name:string):UpdatablePerson = UpdatablePerson(rawPtr:newUpdatablePerson(id, name.cstring))
proc getUpdatablePersonId(self:UpdatablePersonPtr):int64 {.dynlib:libpath, importc:"get_updatable_person_id".}
proc id*(self:UpdatablePerson):int = self.rawPtr.getUpdatablePersonId().int
proc setUpdatablePersonId(self:UpdatablePersonPtr, id:int) {.dynlib:libpath, importc:"set_updatable_person_id".}
proc setId*(self:UpdatablePerson, id:int) = self.rawPtr.setUpdatablePersonId(id)
proc getUpdatablePersonName(self:UpdatablePersonPtr):cstring {.dynlib:libpath, importc:"get_updatable_person_name".}
proc name*(self:UpdatablePerson):string = $self.rawPtr.getUpdatablePersonName()
proc setUpdatablePersonName(self:UpdatablePersonPtr, name:cstring) {.dynlib:libpath, importc:"set_updatable_person_name".}
proc setName*(self:UpdatablePerson, name:string) = self.rawPtr.setUpdatablePersonName(name.cstring)
呼び出します。
import std/unittest
import ./rustlib
suite "object":
test "updatable person":
let person = UpdatablePerson.new(1, "John")
echo person.repr
echo person.id()
echo person.name()
check:
person.id() == 1
person.name() == "John"
person.setId(2)
person.setName("Paul")
echo person.repr
echo person.id()
echo person.name()
check:
person.id() == 2
person.name() == "Paul"
[Suite] object
UpdatablePerson(rawPtr: UpdatablePersonPtr(id: 1, name: "John"))
1
John
UpdatablePerson(rawPtr: UpdatablePersonPtr(id: 2, name: "Paul"))
2
Paul
[OK] updatable person
セッターを使っても上手く呼び出すことができました。
Rustのライブラリを使う
これまでは独自に実装した処理を呼び出してきましたが、本当にやりたいことはRustにある豊富なライブラリの資産をNimから使うことです。
ブロックチェーン領域で使われる楕円曲線暗号を実装したライブラリをNimから呼び出してみましょう。
秘密鍵を作る
Ethereumで使われる秘密鍵とは0〜255までの数字(8bit)が32個並んだ、256bit(32byte)の乱数です。
Rust側
cargo add p256 rand_core hex
use hex::decode as hex_decode;
use hex::encode as hex_encode;
use p256::ecdsa::signature::{Signer, Verifier};
use p256::ecdsa::{Signature, SigningKey, VerifyingKey};
use rand_core::OsRng;
use std::ffi::c_char;
use crate::submods::c_ffi::{cstirng_to_string, string_to_cstring};
#[no_mangle]
pub extern "C" fn create_secret_key() -> *mut Vec<u8> {
let secret_key: SigningKey<NistP256> = SigningKey::random(&mut OsRng);
let v: Vec<u8> = secret_key.to_bytes().to_vec();
Box::into_raw(Box::new(v))
}
#[no_mangle]
pub extern "C" fn get_secret_key_len(v: &mut Vec<u8>) -> usize {
v.len()
}
#[no_mangle]
pub extern "C" fn get_secret_key_item(v: &mut Vec<u8>, offset: usize) -> u8 {
v[offset]
}
秘密鍵は8bitの数字が32個並んだ配列です。フィボナッチ数列の例と同じように、Vector
のポインタとしてNimに渡し、長さとオフセットから単体の値を取り出しNim側ではSeq
として復元します。
Nim側
type SecretKey = ptr object
proc createSecretKeyLib():SecretKey {.dynlib:libpath, importc:"create_secret_key".}
proc len(self:SecretKey):int {.dynlib:libpath, importc:"get_secret_key_len".}
proc `[]`(self:SecretKey, offset:int):uint8 {.dynlib:libpath, importc:"get_secret_key_item".}
proc createSecretKey*():seq[uint8] =
let secretKey = createSecretKeyLib()
defer: secretKey.dealloc()
var s = newSeq[uint8](secretKey.len())
for i in 0..<secretKey.len().int:
s[i] = secretKey[i]
return s
import std/unittest
import ./rustlib
suite "crypto":
test "secret key":
let secretKey = createSecretKey()
echo secretKey
[Suite] crypto
@[39, 234, 215, 165, 187, 41, 126, 106, 147, 128, 126, 120, 235, 187, 243, 63, 97, 84, 236, 27, 126, 195, 100, 93, 40, 90, 142, 186, 63, 11, 152, 44]
[OK] secret key
秘密鍵を作る2
秘密鍵は通常、0x
から始まる16進数の文字列として扱われるので、その形で出力されるようにします。
#[no_mangle]
pub extern "C" fn create_secret_key_hex() -> *mut c_char {
let secret_key: SigningKey<NistP256> = SigningKey::random(&mut OsRng);
let bytes: GenericArray<u8, {unknown}.> = secret_key.to_bytes();
let slices: &[u8] = bytes.as_slice();
let hex_str: String = hex_encode(&slices);
string_to_cstring(hex_str)
}
Nim側
proc createSecretKeyHexLib():cstring {.dynlib:libpath, importc:"create_secret_key_hex".}
proc createSecretKeyHex*():string = "0x" & $createSecretKeyHexLib()
createSecretKeyHex
関数で先頭に0x
を付けています。
import std/unittest
import ./rustlib
suite "crypto":
test "hex key":
let key = createSecretKeyHex()
echo key
0xa44401854dad16e2f56bd8e637a550f6c0904393ac6cb4286e4e3dc5ebf4f3ed
[OK] hex key
出力されました。
署名して、署名を検証する
署名とは、ある文章がある秘密鍵で暗号化されたかどうかを確認することです。
秘密鍵で暗号化された文章は同じ秘密鍵から作られた公開鍵でしか複合できません。
その文章が本当にその秘密鍵を持つ人によって暗号化されたかどうかを確認することを検証といいます。
Rust側
秘密鍵から公開鍵を作る関数、署名する関数、検証する関数の3つを作ります。
- 秘密鍵を使って文章を署名する
- 秘密鍵から公開鍵を作る
- 公開鍵と元の文章、署名から作られたハッシュから署名を検証する
という流れになります。
#[no_mangle]
pub extern "C" fn create_verifying_key(_secret_key: &mut c_char) -> *mut c_char {
let str_secret_key: String = cstirng_to_string(_secret_key);
let b_key: &Vec<u8> = &(hex_decode(str_secret_key).unwrap());
let signing_key: SigningKey<NistP256> = SigningKey::from_bytes(b_key).unwrap();
let verifying_key: VerifyingKey<NistP256> = signing_key.verifying_key();
let encoded_point: EncodedPoint<{unknown}> = verifying_key.to_encoded_point(true);
let str_signature: Stirng = encoded_point.to_string();
string_to_cstring(str_signature)
}
#[no_mangle]
pub extern "C" fn sign_message(_secret_key: &mut c_char, _msg: &mut c_char) -> *mut c_char {
let str_secret_key: String = cstirng_to_string(_secret_key);
let b_key: &Vec<u8> = &(hex_decode(str_secret_key).unwrap());
let signing_key: SigningKey<NistP256> = SigningKey::from_bytes(b_key).unwrap();
let msg: String = cstirng_to_string(_msg);
let b_msg: &[u8] = msg.as_bytes();
let verifying_key: Signature<NistP256> = signing_key.sign(b_msg);
let str_signature: String = verifying_key.to_string().to_lowercase();
string_to_cstring(str_signature)
}
#[no_mangle]
pub extern "C" fn verify_sign(
_verifying_key: &mut c_char,
_msg: &mut c_char,
_signature: &mut c_char,
) -> bool {
let str_verifying_key: String = cstirng_to_string(_verifying_key);
let b_key: &Vec<u8> = &(hex_decode(str_verifying_key).unwrap());
let slice_b_key: &[u8] = b_key.as_slice();
let verifying_key: VerifyingKey<Nist256> = match VerifyingKey::from_sec1_bytes(slice_b_key) {
Ok(verifying_key: VerifyingKey<Nist256>) => verifying_key,
Err(_e: Error) => return false,
};
let msg: String = cstirng_to_string(_msg);
let b_msg: &[u8] = msg.as_bytes();
let str_signature: String = cstirng_to_string(_signature);
let vec_signature: Vec<u8> = hex_decode(str_signature).unwrap();
let b_signature: &[u8] = vec_signature.as_slice();
let signature: Signature<Nist256> = match Signature::try_from(b_signature) {
Ok(signature: Signature<Nist256>) => signature,
Err(_e: Error) => return false,
};
verifying_key.verify(b_msg, &signature).is_ok()
}
Nim側
proc createVerifyingKeyLib(secret:cstring):cstring {.dynlib:libpath, importc:"create_verifying_key".}
proc createVerifyingKey*(secret:string):string =
let secret = secret[2..^1] # 先頭の0xを削除
return "0x" & $createVerifyingKeyLib(secret.cstring)
proc signMessageLib(key, msg:cstring):cstring {.dynlib:libpath, importc:"sign_message".}
proc signMessage*(key, msg:string):string =
let key = key[2..^1] # 先頭の0xを削除
return "0x" & $signMessageLib(key.cstring, msg.cstring)
proc verifySignLib(verifyKey, msg, signature:cstring):bool {.dynlib: libpath, importc:"verify_sign".}
proc verifySign*(verifyKey, msg, signature:string):bool =
let verifyKey = verifyKey[2..^1 ]# 先頭の0xを削除
let signature = signature[2..^1] # 先頭の0xを削除
return verifySignLib(verifyKey.cstring, msg.cstring, signature.cstring)
import std/unittest
import ./rustlib
suite "crypto":
test "verifying key":
let secret = createSecretKeyHex()
echo "=== secret key"
echo secret
echo "=== verify key"
echo createVerifyingKey(secret)
test "sign message":
let msg = "Hello World"
let secretKey = createSecretKeyHex()
let signature = signMessage(secretKey, msg)
echo "=== signature"
echo signature
let verifyKey = createVerifyingKey(secretKey)
echo "=== verify key"
echo verifyKey
let isValid = verifySign(verifyKey, msg, signature)
echo "=== expect true"
echo isValid
check isValid
test "wrong message":
let msg = "Hello World"
let secret = createSecretKeyHex()
let signature = signMessage(secret, msg)
echo "=== signature"
echo signature
let verifyKey = createVerifyingKey(secret)
echo "=== verify key"
echo verifyKey
let res = verifySign(verifyKey, "wrong hello", signature)
echo "=== expect false"
echo res
check res == false
test "wrong signature":
let msg = "Hello World"
let secret = createSecretKeyHex()
let signature = signMessage(secret, msg)
echo "=== signature"
echo signature
var expectWrong = verifySign("0x012345abcdef", msg, signature)
echo "=== expect false"
echo expectWrong
check expectWrong == false
=== secret key
0x61ee88fb30fe88e1bd0bafae57f78811c678b58a55401c5e64c714f8907da3a6
=== verify key
0x035C687146BF98F3935AA4E0B267522765ED7C15B17FC08372E115869D92922615
[OK] verifying key
=== signature
0xf1f6bbe1345faaa3c3514b6ca01324602d9ab0344b38439574fda2b70a3c092462ffef099a068126aa8764637f9efce89554a94018f7c56d2f26210b120da33d
=== verify key
0x03EB937AF6C821116418A7BEF874974BED79ED43AC39B2D5CE28802C1971AC3BBC
=== expect true
true
[OK] sign message
=== signature
0x606bb9b3b9094057aadc2f4563923fdfc6d4a73f6991e530e3e60fc346c2d4245c2544be8dabb0535fe8cab0b8119b8920cf89a44e5f518bbe4f5c86b435be5a
=== verify key
0x0253FF110C708A36E15F18B4784E48473B3EC74485CD1E6D0AA989580CEF4F65CF
=== expect false
false
[OK] wrong message
=== signature
0xb83a17ac892234b3b840c8d45cd2a8e1d4b68601d2a3dc52cad4fa86c13116150cc8288b0ffed750e0af45cd8d600875b06b1db0c4f7077828927b3d34155433
=== expect false
false
[OK] wrong signature
正しく署名の検証ができました。
おわりに
NimとRustのFFIの機能を使って相互に値をやりとりできることがわかりました。
これでNimでRustの資産が使えます! どしどしNimでRustをラップしたライブラリを作り、Nimでアプリケーションを作っていきましょう!!!
感想としては、FFIをするためのRust側での型パズル、ポインタはNim側で明示的に開放しなければいけないことが少し難しいかなと思いました。
数値側やboolはほぼそのままで大丈夫なんですが、ヒープに積まれる文字列、配列、独自型については以下のようにすると扱えるようです。
型 | Nimの引数 | Nimの返り値 | Rustの引数 | Rustの返り値 |
---|---|---|---|---|
文字列 | cstring | cstring | &mut c_char | *mut c_char / *const c_char |
配列 | type T = ptr object | type T = ptr object | &mut Vec<T> | *mut Vec<T> |
独自型 | type T = ptr object | type T = ptr object | &T / &mut T | *mut T |
またRustにFFIを楽にするsafer_ffi
というライブラリがあり、そちらも使ってみましたが、まだライブラリが未熟なようで、Rustの関数で引数を受け取ることができなかったりしました。
このライブラリがちゃんと使えるようになると、Rustの関数からCのヘッダーファイルを出力し、Cのヘッダーファイルからc2nimを使ってNimのインターフェース関数を自動生成できるようになるので、今後の発展に期待していきたいです。
Discussion