DEV Community

Cover image for The way integrate Rust into Nim
medy
medy

Posted on

The way integrate Rust into Nim

This is the English version of this article.
https://zenn.dev/dumblepy/articles/3db2134ff88763

Motivation

Nim is a programming language with a simple Python-like syntax that can be transpiled to C and compiled to binary, combining low learning cost, high development productivity, and fast execution speed.
The compiler automatically performs safe scope-based memory management based on ownership and borrowing, and since there is no need to think about references and pointers, "code for coding's sake" can be reduced, especially in application development, and the description can focus solely on the business logic.
However, it is not yet widely used, and when we ask those who do not use it, we often hear that the reason is that "there are not enough libraries".
Nim can easily incorporate assets that already exist in C, because it converts to C once at compile time, and it can work very seamlessly with both dynamic linking and static archives.

Rust, on the other hand, is a thoroughly memory-safe language at the lowest levels, preventing segfaults and memory leaks and running very fast.
However, variable ownership and borrowing must be considered during development, and it is very expensive to learn. It is not a language that is easy to learn, at least not by a second year PHPer with a liberal arts background.

I think it would be a good idea to use Rust for libraries, such as implementing math-based algorithms, and Nim for applications.

Since both Nim and Rust have a mechanism for FFI via the C language, we will use it to experiment with calling libraries created in Rust from Nim applications.

I am just a beginner with 1 week experience of Rust, I have touched Nim for a long time, but I come from a PHPer background with no C experience and have only done LL languages.
It is possible that I am writing incorrectly about Rust usage and memory management.
If you find any, please feel free to comment.

Build an environment

Create a Docker container with both Nim and Rust environments.

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
Enter fullscreen mode Exit fullscreen mode

Create a project

Create a src directory under /application and work from there.

Create a project with Nim

cd /application/src
nimble init nimapp
Enter fullscreen mode Exit fullscreen mode

You will be asked interactively, so use Tab to cycle through the choices and Enter to select one.
For Package type?, choose 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
Enter fullscreen mode Exit fullscreen mode

Creating a project in Rust

cd /application/src
cargo new rustlib --lib
Enter fullscreen mode Exit fullscreen mode

The directory structure will look like this

/application
`-- src
    |-- nimapp
    |   |-- nimapp.nimble
    |   |-- src
    |   |   `-- nimapp.nim
    |   `-- tests
    |       |-- config.nims
    |       `-- test1.nim
    `-- rustlib
        |-- Cargo.toml
        `-- src
            `-- lib.rs
Enter fullscreen mode Exit fullscreen mode

Calling a function

Let's start with a simple add function that adds ints.

Rust side

// lib.rs

#[no_mangle]
pub extern "C" fn add(a: i64, b: i64) -> i64 {
    return a + b;
}
Enter fullscreen mode Exit fullscreen mode
#[no_mangle]
Enter fullscreen mode Exit fullscreen mode

By attaching this to a function, it can be called from C/Nim with a function name of add as defined in Rust.

pub extern "C"
Enter fullscreen mode Exit fullscreen mode

By attaching this to a function, it becomes a function that can be called from C/Nim.

# 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"]
Enter fullscreen mode Exit fullscreen mode

Set crate-type when building libraries.
cdylib if you are compiling into a dynamic library, or staticlib for a static archive.

https://doc.rust-lang.org/nomicon/ffi.html#rust-side

Compile.

cd /application/src/rustlib
cargo build --release
Enter fullscreen mode Exit fullscreen mode

A Shard Object file has been output to /application/src/rustlib/target/release/librustlib.so. This is used by calling it from Nim.

Nim side

Create a file /application/src/nimapp/src/rustlib.nim and define the glue functions so that the functions in the shard object can be called from Nim.

# rustlib.nim

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

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

This is how when you call a static archive.

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

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

All that is left is to call this add function from within nimapp.nim.

# nimapp.nim

import std/unittest
import ./rustlib

suite "test":
  test "add":
    echo add(1, 2)
    check add(1, 2) == 3
Enter fullscreen mode Exit fullscreen mode

Let's execute it.

cd /application/src/nimapp
nim c -r -f --mm:orc src/nimapp
Enter fullscreen mode Exit fullscreen mode
# output

[Suite] test
3
  [OK] add
Enter fullscreen mode Exit fullscreen mode

I was able to call it up.

Working with dynamic arrays

How can we handle Rust's Vector with Nim?
Here is an explanation using a function that returns a Fibonacci sequence.

Rust side

Define a function that returns a Fibonacci number, and a function that calls it internally to return the Fibonacci sequence.

// 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]
}
Enter fullscreen mode Exit fullscreen mode

The return type of fib_array should be *mut Vec<u64> and at the end of the function, call Box::into_raw(Box::new(vector)) to return a raw pointer to the heap.
We also implement a function that returns the length and offset position values from the vector.

Nim side

# 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
Enter fullscreen mode Exit fullscreen mode

The return value of Rust's get_fib_len is a raw pointer to the heap, so we define our own object as FibPtr to map to it.
All Nim functions work with static type checking and overloading, so any functions defined here will only work on objects of type FibPtr.
The fibArray function calls the functions defined on the Rust side to get the vector length and offset position values from the raw pointer, fill it into Seq(Sequence), Nim's dynamic array, and return it.
In Nim, the raw pointer is outside the scope of Nim's memory management. There is a dealloc function to free memory for pointers, and you can use defer to make sure that memory is freed when you leave scope.
This defer is the same as in the Go language.

Now let's call it in 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]
Enter fullscreen mode Exit fullscreen mode
cd /application/src/nimapp
nim c -r -f --mm:orc src/nimapp
Enter fullscreen mode Exit fullscreen mode
# output

[Suite] test
3
  [OK] add
@[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
  [OK] fib array
Enter fullscreen mode Exit fullscreen mode

I was able to call it up.

Move processing to a submodule

We have written the add function and the Fibonacci sequence output function in lib.rs, but you can move them to a submodule.
You can also move them to a submodule, as this makes the code more readable.

Let's make the Rust directory structure look like this.

.
|-- Cargo.lock
|-- Cargo.toml
`-- src
    |-- lib.rs
    `-- submods
        `-- fib.rs
Enter fullscreen mode Exit fullscreen mode

Move the process to the fib.rs file.

// submods/fib.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_vector_len(v: &Vec<u64>) -> usize {
    v.len()
}

#[no_mangle]
pub extern "C" fn get_vector_item(v: &Vec<u64>, offset: usize) -> u64 {
    v[offset]
}
Enter fullscreen mode Exit fullscreen mode

lib.rs should look like this.

// lib.rs

mod submods {
    pub mod fib;
}

#[no_mangle]
pub extern "C" fn add(a: i64, b: i64) -> i64 {
    return a + b;
}
Enter fullscreen mode Exit fullscreen mode

Handle custom types (proprietary types, structs)

Allows Nim to handle instances of structs defined in Rust.

Rust side

Create a submods/person.rs file.
Define a type person with numeric and string fields, its constructor and getter methods.
The function names to be output to FFI should be named so as not to cover as much as possible. For this reason, the name of the method that returns the id is not id but 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;
  }
Enter fullscreen mode Exit fullscreen mode
// 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");
    }
}
Enter fullscreen mode Exit fullscreen mode

The type of the name argument of the new_person function is *const c_char. This type is used to handle C strings in Rust.
Conversely, to return a string from Rust to C, use *mut c_char.

The return type of the new_person function is *mut person. This is a raw heap pointer, like the Fibonacci sequence described above.

Both Nim strings and Rust strings are unique types that only work within the execution environment of each language.
Therefore, in order to exchange strings from Nim to Rust via C, they must be converted to each other.
Here, we first created a function on the Rust side to convert C strings to each other's strings.

// 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()
}
Enter fullscreen mode Exit fullscreen mode

In person.rs this is called.

Nim side

# 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()
Enter fullscreen mode Exit fullscreen mode

Define the same structure in Nim's object as in Rust's structure definition.

Since it is the raw pointer of the heap that actually interacts with Rust functions, we define a pointer object PersonPtr to map to it.
Pointers are outside of Nim's memory management jurisdiction, but ref objects with a pointer type field are automatically managed, so you can define Person* = ref object to handle them from Nim. This eliminates the need to deallocate memory explicitly.

The type of the name argument of newPerson is cstring. This corresponds to a C string in Nim, and can be type-converted as "string".cstring.

In the name function we call getPersonName, but the return type of getPersonName is cstring, so we add $ to convert it to string. $ is a magic method in the Nim world that converts any type to a string. (In fact, it is implemented to convert all types to string with the same function name $)

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()
Enter fullscreen mode Exit fullscreen mode

These three functions are glue code that is called by the Nim application to call functions like newPerson that are mapped to Rust functions and do type conversion.

Let's call them.

# 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"
Enter fullscreen mode Exit fullscreen mode
# output

[Suite] object
Person(rawPtr: PersonPtr(id: 1, name: "John"))
1
John
  [OK] person
Enter fullscreen mode Exit fullscreen mode

Both mapping values to fields in the `PersonPtr' object and calling functions are working well.

Handling proprietary types with setters

So far we have only dealt with instance creation and getter methods, but does it work with setter methods?
We will illustrate this using the `UpdatablePerson' type, which can update fields.

Rust side

  // 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;
  }
Enter fullscreen mode Exit fullscreen mode
// 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");
    }
}
Enter fullscreen mode Exit fullscreen mode

Nim side

# 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)
Enter fullscreen mode Exit fullscreen mode

Call.

# 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"
Enter fullscreen mode Exit fullscreen mode
# output

[Suite] object
UpdatablePerson(rawPtr: UpdatablePersonPtr(id: 1, name: "John"))
1
John
UpdatablePerson(rawPtr: UpdatablePersonPtr(id: 2, name: "Paul"))
2
Paul
  [OK] updatable person
Enter fullscreen mode Exit fullscreen mode

Using a setter, it was successfully invoked.

Using Rust's libraries

So far we have been calling our own implementation of the process, but what we really want to do is use Rust's rich library assets from Nim.
Let's call a library from Nim that implements the elliptic curve cryptography used in the blockchain domain.

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

Creating a private key

The private key used in Ethereum is a 256-bit (32-byte) random number consisting of 32 numbers (8 bits) ranging from 0 to 255.

Rust side

cargo add p256 rand_core hex
Enter fullscreen mode Exit fullscreen mode
// 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]
}
Enter fullscreen mode Exit fullscreen mode

The secret key is an array of 32 8-bit numbers. As in the Fibonacci sequence example, it is passed to Nim as a pointer to a vector', and the length and offset are used to extract a single value and return it as aseq' on the Nim side.

Nim side

# 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
Enter fullscreen mode Exit fullscreen mode
# nimapp.nim

import std/unittest
import ./rustlib


suite "crypto":
  test "secret key":
    let secretKey = createSecretKey()
    echo secretKey
Enter fullscreen mode Exit fullscreen mode
# output

[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
Enter fullscreen mode Exit fullscreen mode

create private key 2

Private keys are usually treated as a hexadecimal string starting with 0x, so they should be output in that form.

// 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)
}
Enter fullscreen mode Exit fullscreen mode

Nim側

# rustlib.nim

proc createSecretKeyHexLib():cstring {.dynlib:libpath, importc:"create_secret_key_hex".}
proc createSecretKeyHex*():string = "0x" & $createSecretKeyHexLib()
Enter fullscreen mode Exit fullscreen mode

0x is prepended in the createSecretKeyHex function.

# nimapp.nim

import std/unittest
import ./rustlib


suite "crypto":
  test "hex key":
    let key = createSecretKeyHex()
    echo key
Enter fullscreen mode Exit fullscreen mode
# output

0xa44401854dad16e2f56bd8e637a550f6c0904393ac6cb4286e4e3dc5ebf4f3ed
  [OK] hex key
Enter fullscreen mode Exit fullscreen mode

Output successfully.

Signing and Verifying Signatures

Signing is the process of verifying that a certain text has been encrypted with a certain private key.
A sentence encrypted with a private key can only be signed with a public key generated from the same private key.
Verifying that a sentence was actually encrypted by someone using that private key is called verification.

Rust side

Create three functions: a function to create a public key from a private key, a function to sign a document, and a function to verify a document.

  1. sign a text with the private key
  2. generate a public key from the private key
  3. Verify the signature using the hash generated from the public key, the original text and the signature. The process is as follows.
// 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()
}
Enter fullscreen mode Exit fullscreen mode

Nim side

# rustlib.nim

proc createVerifyingKeyLib(secret:cstring):cstring {.dynlib:libpath, importc:"create_verifying_key".}
proc createVerifyingKey*(secret:string):string =
  let secret = secret[2..^1] # remove "0x" prefix
  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] # remove "0x" prefix
  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 ]# remove "0x" prefix
  let signature = signature[2..^1] # remove "0x" prefix
  return verifySignLib(verifyKey.cstring, msg.cstring, signature.cstring)
Enter fullscreen mode Exit fullscreen mode
# 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
Enter fullscreen mode Exit fullscreen mode
# 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
Enter fullscreen mode Exit fullscreen mode

The signature has been verified correctly.

Conclusion

We now know that we can use the FFI feature of Nim and Rust to exchange values with each other.
Now we can use Rust's resources in Nim! Let's build a library that wraps Rust in Nim and build applications in Nim!!!

I thought it was a bit difficult to do type puzzles on the Rust side for FFI, and that pointers had to be explicitly opened on the Nim side.
The numeric side and bool are almost fine as is, but strings, arrays, and unique types that are stacked on the heap can be handled by doing the following.

type Nim's argument Nim's return value Rust's argument Rust's return value
String cstring cstring &mut c_char *mut c_char / *const c_char
Array type T = ptr object type T = ptr object &mut Vec *mut Vec
unique type type T = ptr object type T = ptr object &T / &mut T *mut T

Rust also has a library called safer_ffi that makes FFI easier, and I tried to use that, but the library seems to be immature, and I could not get arguments in Rust functions.
If this library can be used properly, it will be possible to output C header files from Rust functions and automatically generate Nim interface functions from C header files using c2nim. We look forward to further development of this feature.

Latest comments (0)