Calling Cardano-Rust from C

There is an implementation of Cardano written in Rust here: IOHK Cardano-Rust github

Rust can be compiled to many platforms including Windows, Linux, Android and iOS. You can also write functions in Rust that can be called as if they were C functions. Almost all languages allow you to call C library functions. That means you can call the Cardano code from (almost) any language on (almost) any platform.

Obviously data-type representation is different in different languages. We need to write a “bridging” function in Rust, that converts data, calls the functionality, converts data back. In the case of calling from C, we just need “half” the bridge; we only need Rust to talk to C. If we wanted to call the Rust code from another language X, we would need to also make the X-talking-to-C part.

The Rust Side

I downloaded the code from github, under the top level directory I added a cardano-test subdirectory. I add the cardano-test workspace to the main Cargo.toml:

 [workspace]
 members = [
    "cardano",
    "protocol",
    "storage-units",
    "storage",
    "hermes",
    "cardano-test"
]
[dependencies]
printer = { path = "rust-crypto-wasm" }

Under cardano-test I created this Cargo.toml, basically a copy of the one in cardano-c, package name is test:

[package]
name = "test"
version = "0.1.0"

[lib]
crate-type = ["staticlib"]

[dependencies]
hex = "0.3.2"
cryptoxide = "0.1"
cbor_event = "1.0"
libc = "*"
serde = "1.0"
serde_derive = "1.0"
rand = "0.4"
serde_json = "*"
unicode-normalization = "0.1.7"
cardano = { path = "../cardano" }
storage-units = { path = "../storage-units" }

Under cardano-test I make a src and c subdirectories. I’ve picked the base58 encode & decode functions from the Cardano-Rust code as an example.

In src/ I create a new Rust code file b58.rs which has 2 special functions that call the Rust-Cardano code, these functions are callable from C:

extern crate libc;
use std::os::raw::{c_char};
use std::{ffi, ptr, slice};
use cardano::util::base58::*;
use std::ffi::CStr;
use std::ffi::CString;
  
#[no_mangle]
pub extern "C"
fn my_b58_encode(p: *const u8, p_size: u32, encoded: *mut c_char) -> i8
{
  let p_slice = unsafe { slice::from_raw_parts(p, p_size as usize) };

  let str = encode(p_slice);  /* CALL */

  let bytes = str.as_bytes();
  let cs = CString::new(bytes).unwrap();  // will fail if bytes has "gap" (null) in sequence
  unsafe {
    libc::strcpy( encoded, cs.as_ptr() );
  }

  return 1;
}

#[no_mangle]
pub extern "C"
fn my_b58_decode(encoded: *const c_char, p: *mut u8) -> i8
{
  let c_str: &CStr = unsafe { CStr::from_ptr(encoded) };
  let tmp_str: &str = c_str.to_str().unwrap();  // will fail if "gap" (null) in string, prevent in caller
  let str: String = tmp_str.to_owned();

  let vec = decode(&str);  /* CALL */

  if vec.is_err() {
    return -1; 
  }
 
  let mut tmp_ptr_u8 = p as *mut u8;

  let mut s: u32 = 0;

  for byte in vec.unwrap() {
    s=s+1;
    unsafe {
      *tmp_ptr_u8 = byte;
      tmp_ptr_u8 = tmp_ptr_u8.offset(1);
    }
  }

  return 1;
}

I’m not familiar with Rust, perhaps it could be done better, I don’t know. The function my_b58_encode takes a C pointer to size bytes, and a pointer to allocated memory for the output. my_b58_decode takes a pointer to a string, and a pointer to allocated and initialised memory for the output.

I create a second file under src/ called lib.rs:

extern crate cardano;

pub mod b58;
pub use b58::*;

From cardano-test, I execute “cargo build” to make the library for the target platform. You can see platform list: rustc –print target-list. I am using a mac, so I make it for x86_64-apple-darwin:

pusheen: cargo build --target x86_64-apple-darwin
   Compiling libc v0.2.43
   Compiling cbor_event v1.0.0
   ...

Building produces the library file under target/ in the parent directory (weird right?). The filename is lib[package_name].a (or .so depending on config/platform), the “package_name” is as specified in Cargo.toml, which controls the build.

pusheen: ls -lrt ../target/x86_64-apple-darwin/debug/libtest.a
-rw-r--r--  2 pusheen  staff  18725848 19 Oct 12:41 ../target/x86_64-apple-darwin/debug/libtest.a

The new functions can be seen in the library:

pusheen: nm ../target/x86_64-apple-darwin/debug/libtest.a | grep my_b58_
0000000000000180 T _my_b58_decode
0000000000000000 T _my_b58_encode

The C Side.

Now we need to call the functions from C. Under cardano-test make a header file cardano.h with the function definitions:

int8_t my_b58_encode( const uint8_t *bytes, unsigned int size, char *encoded );
int8_t my_b58_decode( const char *encoded, uint8_t *bytes );

For convenience I create a soft link to the library and header file:

ln -s ../../target/x86_64-apple-darwin/debug/libtest.a ./libtest.a

Then I make a C file with a main function that calls our functions:

pusheen: cat my_b58_test.c 
#include 
#include 
#include 
#include "../cardano.h"

int8_t bytes_to_hex( uint8_t*, unsigned int, char *);

int main() {

  char *plain1 = "Hello World!";
  char *ebuffer = (char *)malloc(1000);
  memset(ebuffer,'\0',1000);
 
  printf("\nData to encode: %s \n",plain1);
  my_b58_encode((uint8_t *)plain1,strlen(plain1),ebuffer);

  printf("Encoded: %s \n",ebuffer);

  uint8_t *dbuffer = (uint8_t *)malloc(1000);
  memset(dbuffer,'\0',1000);

  my_b58_decode(ebuffer,(uint8_t *)dbuffer);
   
  printf("Decoded again: %s \n\n",dbuffer);

  free(ebuffer);
  free(dbuffer);
}

We can compile, link the binary:

gcc -g my_b58_test.c libtest.a 

Run the binary:

pusheen: ./a.out

Data to encode: Hello World! 
Encoded: 2NEpo7TZRRrLZSi2U 
Decoded again: Hello World! 

Checking can be done using this handy online tool

Code for this example is here

An observation: Be careful to allocate and free from the same language runtime; if you allocated in Rust you must free in Rust, and vice versa, otherwise you will get strange periodic runtime errors. This sounds simple but may not be, initially I tried creating a Rust vec datatype on the back of data at a C pointer, it worked but occasionally it bombed. Seems like Rust manages the memory for vec internally, how exactly I don’t know, but looks like sometimes it is freeing and reallocating. That wouldn’t be an issue if your whole program was written in Rust, you’d never know, but in this case it’s a seggy.

That is the end of this simple example of calling Cardano-Rust from C. Next we will build a bridge to Android/Java and call the functions from there.