Rust Programming with Dynamsoft Barcode Reader

A few days ago, I accepted a new challenge of creating a simple command line barcode reader using Rust and Dynamsoft Barcode Reader SDK. Rust is a system programming language similar to C++. The learning process did not go through smoothly as I expected. It is not as easy as learning other high-level programming languages such as Java and Python. In this article, I share my experience of learning and using Rust.

Prerequisites

Building Rust Barcode Reader on Windows

Project initialization

Create a new project:

cargo new barcodereader

In main.rs, get the input file name from arguments:

use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();
    if args.len() == 1 {
        println!("Please input an image file.");
        return
}
let file_name = env::args().nth(1).expect("Missing argument");
    println!("{}", file_name);

}

Build and run the app:

cargo run .\images\AllSupportedBarcodeTypes.tif

Bridging Rust and native methods

To simplify the invoking process, create bridge.h and bridge.c.

// bridge.h
typedef struct Barcode {
    char* barcode_type;
    char* barcode_value;
} Barcode;

typedef __int32 int32_t;
typedef void (*RustCallback)(int32_t, const char *, const char *);

int32_t register_callback(RustCallback callback);
void c_decodeFile(const char *fileName, const char *pszLicense);
// bridge.c
#include "bridge.h"
#include "DynamsoftBarcodeReader.h"
#include <stdio.h>

RustCallback cb;

int32_t register_callback(RustCallback callback) {
    cb = callback;
    return 1;
}

void c_decodeFile(const char *fileName, const char *pszLicense)
{
    void *hBarcode = DBR_CreateInstance();

    if (hBarcode)
    {
        int ret = DBR_InitLicense(hBarcode, pszLicense);
        STextResultArray *paryResult = NULL;
        ret = DBR_DecodeFile(hBarcode, fileName, "");
        DBR_GetAllTextResults(hBarcode, &paryResult);
        int count = paryResult->nResultsCount;
        printf("Barcode found: %d\n", count);
        int i = 0;
        for (; i < count; i++)
        {
            if (cb) 
            {
                cb(i, paryResult->ppResults[i]->pszBarcodeFormatString, paryResult->ppResults[i]->pszBarcodeText);
            }
        }
        DBR_FreeTextResults(&paryResult);
        DBR_DestroyInstance(hBarcode);
    }
}

Here I use a callback function to pass the barcode decoding results from C to Rust. Add the callback function to main.rs:

use std::ffi::CStr;

extern "C" fn callback(index: i32, barcode_type: *const c_char, barcode_value: *const c_char) {
    unsafe {
        println!(
            "Index {}, {}, {}",
            index,
            CStr::from_ptr(barcode_type).to_str().unwrap(),
            CStr::from_ptr(barcode_value).to_str().unwrap()
        );
    }
}

To link the native library and build the bridge, create build.rs in the project root:

extern crate cc;

fn main() {
    // Link Dynamsoft Barcode Reader.
    println!("cargo:rustc-link-search=./platforms/win");
    println!("cargo:rustc-link-lib=DBRx64");

    // Build C code.
    cc::Build::new()
        .include("include")
        .file("src/bridge.c")
        .compile("bridge");
}

Add the configuration to Cargo.toml:

[package]
# ...
build = "build.rs"
[build-dependencies]
cc = "1.0"

Why not call native methods directly? It is complicated. If you do this, you have to define all C structures in Rust. The next step is to generate Rust FFI bindings based on the bridge.h file.

Converting C header files to Rust FFI bindings

Use bindgen to convert bridge.h to bridge.rs:

bindgen.exe --no-doc-comments --no-layout-tests bridge.h -o bridge.rs

When I ran the above command line the first time, I got the following error message:

error: toolchain 'stable-x86_64-pc-windows-msvc' does not have the binary `rustfmt.exe`

To fix the issue, install rustfmt-preview:

rustup component add rustfmt-preview

We can automate the process by adding bindgen as a build dependency in Cargo.toml:

[build-dependencies]
bindgen = "0.42.1"

Then add the following code to build.rs:

extern crate bindgen;
fn main() {
	// Generates Rust FFI bindings.
    let bindings = bindgen::Builder::default()
        // The input header we would like to generate
        // bindings for.
        .header("src/bridge.h")
        // Finish the builder and generate the bindings.
        .generate()
        // Unwrap the Result and panic on failure.
        .expect("Unable to generate bindings");

    bindings
        .write_to_file("src/bridge.rs")
        .expect("Couldn't write bindings!");

}

Automatically copy *.dll files to the output directory after building the project

On Windows, we use .lib files for building projects and use .dll files for running apps. To make the program run, we have to copy DLL files to the output directory.

The first step is to get the path of the output folder:

let out_dir = env::var("OUT_DIR").unwrap();
    let debug_offset = out_dir.find("debug").unwrap_or(0);
    let release_offset = out_dir.find("release").unwrap_or(0);
    let mut path = String::from("");

    if debug_offset > 0 {
        println!(">>> where is debug {}", debug_offset);
        path.push_str(&format!("{}", &out_dir[..debug_offset]));
        path.push_str("debug");
        println!("{}", path);
    }

    if release_offset > 0 {
        println!(">>> where is release {}", release_offset);
        path.push_str(&format!("{}", &out_dir[..release_offset]));
        path.push_str("release");
        println!("{}", path);
    }

Copy DLL files:

let _src: String = String::from("./platforms/win/DynamsoftBarcodeReaderx64.dll");
path.push_str("/DynamsoftBarcodeReaderx64.dll");
let _result = fs::copy(_src, &path);

Build and run Rust barcode reader

Call the barcode detection function in main.rs:

use std::ffi::CStr;
use std::ffi::CString;
use std::os::raw::c_char;
use std::env;

mod bridge;
use bridge::*;

extern "C" fn callback(index: i32, barcode_type: *const c_char, barcode_value: *const c_char) {
    unsafe {
        println!(
            "Index {}, {}, {}",
            index,
            CStr::from_ptr(barcode_type).to_str().unwrap(),
            CStr::from_ptr(barcode_value).to_str().unwrap()
        );
    }
}

fn main() {
    let args: Vec<String> = env::args().collect();
    if args.len() == 1 {
        println!("Please input an image file.");
        return
    }

    println!("Hello Dynamsoft Barcode Reader!");
    unsafe {
        register_callback(Some(callback));
        let image_file = CString::new(env::args().nth(1).expect("Missing argument")).unwrap();
        let license = CString::new("t0068NQAAAFKYHV9xSZDEThUtClXNzxXH9TLSj/vYcY8mSKa0RxaGw3qNynyAMJ9Ib8UPxzFsbAMIugqPO313BvfiOdmZFTY=").unwrap();
        c_decodeFile(image_file.as_ptr(), license.as_ptr());
    }

    println!("Bye!");
}

Build the app:

cargo build -vv

Clean the build:

cargo clean

Run the app:

cargo run <image file>

Rust barcode reader

References

Source Code

https://github.com/yushulx/rust-barcode