👑

RustをNimから呼び出す

2023/01/21に公開

モチベーション

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側

lib.rs
#[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から呼び出せる関数になります。

https://tomoyuki-nakabayashi.github.io/book/interoperability/rust-with-c.html#c-apiの作成

ライブラリを出力する時には、crate-typeを設定します。
動的ライブラリにコンパイルする時にはcdylibを、静的アーカイブにコンパイルする時にはstaticlibにします。

Cargo.toml
[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"]

https://qiita.com/etoilevi/items/4bd4c5b726e41f5a6689

コンパイルします。

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から呼べるようにするためのグルー関数を定義します。

rustlib.nim
const libpath = "/application/src/rustlib/target/release/librustlib.so"

proc add*(a, b:int64):int64 {.dynlib:libpath, importc: "add".}

静的アーカイブを呼び出す時にはこのようにします。

rustlib.nim
const libpath = "/application/src/rustlib/target/release/librustlib.a"

{.passL:libpath.}
proc add*(a, b:int64):int64 {.cdecl, importc: "add".}

あとはnimapp.nimからこのadd関数を呼び出せばいいだけです。

nimapp.nim
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
output
[Suite] test
3
  [OK] add

呼び出すことができました。

動的配列を扱う

RustのVectorをNimで扱うにはどうすればいいでしょうか。
ここではフィボナッチ数列を返す関数を使って説明します。

Rust側

フィボナッチ数を返す関数、それを内部で呼んでフィボナッチ数列を返す関数を定義します。

lib.rs
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側

rustlib.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の中で呼び出してみましょう。

nimapp.nim
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
output
[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としています。

lib.rs
  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;
  }
submods/person.rs
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言語の文字列を相互変換する関数を作りました。

submods/c_ffi.rs
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側

rustlib.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などの関数を呼び出す処理と型変換を行うグルーコードです。

では呼び出してみましょう。

nimapp.nim
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"
output
[Suite] object
Person(rawPtr: PersonPtr(id: 1, name: "John"))
1
John
  [OK] person

PersonPtrオブジェクトのフィールドへの値のマッピングも、関数呼びだしも上手く行っています。

セッターを持つ独自型を扱う

これまでインスタンス生成とゲッターメソッドしか扱っていませんが、セッターメソッドでも上手くいくでしょうか
フィールドを更新することができるUpdatablePerson型を使って説明します。

Rust側

lib.rs
  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;
  }
submods/update_person.rs
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側

rustlib.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)

呼び出します。

nimapp.nim
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"
output
[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から呼び出してみましょう。

https://docs.rs/p256/latest/p256/

秘密鍵を作る

Ethereumで使われる秘密鍵とは0〜255までの数字(8bit)が32個並んだ、256bit(32byte)の乱数です。

https://www.etarou.work/posts/5084927/

Rust側

cargo add p256 rand_core hex
submods/crypto.rs
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側

rustlib.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
nimapp.nim
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進数の文字列として扱われるので、その形で出力されるようにします。

submods/crypto.rs
#[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側

rustlib.nim
proc createSecretKeyHexLib():cstring {.dynlib:libpath, importc:"create_secret_key_hex".}
proc createSecretKeyHex*():string = "0x" & $createSecretKeyHexLib()

createSecretKeyHex関数で先頭に0xを付けています。

nimapp.nim
import std/unittest
import ./rustlib


suite "crypto":
  test "hex key":
    let key = createSecretKeyHex()
    echo key
0xa44401854dad16e2f56bd8e637a550f6c0904393ac6cb4286e4e3dc5ebf4f3ed
  [OK] hex key

出力されました。

署名して、署名を検証する

署名とは、ある文章がある秘密鍵で暗号化されたかどうかを確認することです。
秘密鍵で暗号化された文章は同じ秘密鍵から作られた公開鍵でしか複合できません。
その文章が本当にその秘密鍵を持つ人によって暗号化されたかどうかを確認することを検証といいます。

https://www.jipdec.or.jp/project/research/why-e-signature/public-key-cryptography.html

Rust側

秘密鍵から公開鍵を作る関数、署名する関数、検証する関数の3つを作ります。

  1. 秘密鍵を使って文章を署名する
  2. 秘密鍵から公開鍵を作る
  3. 公開鍵と元の文章、署名から作られたハッシュから署名を検証する
    という流れになります。
submods/crypto.rs
#[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側

rustlib.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)
nimapp.nim
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
output
=== 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のインターフェース関数を自動生成できるようになるので、今後の発展に期待していきたいです。

GitHubで編集を提案

Discussion

ログインするとコメントできます