How to Build a Command-Line Barcode Reader with Rust and Dynamsoft C++ Barcode SDK
Rust’s popularity continues to surge in the systems programming world due to its unique combination of memory safety, performance, and concurrency. This comprehensive article demonstrates how to integrate Rust with the Dynamsoft Capture Vision Router SDK using FFI (Foreign Function Interface) bindings. We’ll build a robust command-line barcode reader that works seamlessly across Windows, Linux, and macOS platforms.
The final implementation supports both interactive mode for testing individual images and batch processing mode for automated workflows, making it suitable for both development and production environments.
This article is Part 13 in a 15-Part Series.
- Part 1 - Building a C/C++ Barcode & QR Code Reader for Raspberry Pi with Dynamsoft SDK
- Part 2 - CMake: Build C++ Project for Windows, Linux and macOS
- Part 3 - Insight Into Dynamsoft Barcode SDK Decoding Performance
- Part 4 - Building ARM64 Barcode and QR Code Scanner on Nvidia Jetson Nano
- Part 5 - How to Decode QR Code on Mac with Apple Silicon
- Part 6 - How to Develop a Desktop GUI Barcode Reader with Qt and C/C++
- Part 7 - How to Build a Desktop Barcode Scanner with Webcam Support Using Qt QCamera
- Part 8 - Building Command-line Barcode and QR Code Reader in C++
- Part 9 - How to Build Linux ARM32 and Aarch64 Barcode QR Scanner in Docker Container
- Part 10 - How to Link MSVC DLLs with MinGW GCC in Windows
- Part 11 - Transforming Raspberry Pi 4 into a Barcode Scanner with a C++ App, USB Camera, and OLED Display
- Part 12 - Building Windows Desktop Barcode Reader with Win32 API and Dynamsoft C++ Barcode SDK
- Part 13 - How to Build a Command-Line Barcode Reader with Rust and Dynamsoft C++ Barcode SDK
- Part 14 - How to Decode Barcode and QR Code from WebP Images in C++ and Python
- Part 15 - Building a Desktop C++ Barcode Scanner with Slimmed-Down OpenCV and Webcam
Prerequisites
- Rust: A systems programming language renowned for memory safety, speed, and fearless concurrency
- Dynamsoft Capture Vision Trial License: Get a 30-day free trial license delivered via email
- Dynamsoft Capture Vision SDK: The latest cross-platform SDK supporting barcode reading, document scanning, and more
- Visual Studio Build Tools (Windows only): Required for C++ compilation and FFI bindings
- Development Tools:
cccrate: For compiling C++ bridge codewalkdircrate: For directory traversal and file operations
Project Overview
Our Rust barcode scanner features:
- Dual Operation Modes: Interactive mode for testing and batch mode for automation
- Cross-Platform Support: Windows, Linux, and macOS compatibility
- Modern SDK Integration: Uses the latest Capture Vision Router API
- Resource Management: Automatic copying of required SDK resources
- Memory Safety: Rust’s ownership system prevents common C++ pitfalls
- Performance: Near-native speed with zero-cost abstractions
Step 1: Setting Up the Rust Project
-
Initialize a new Rust project using Cargo:
cargo new dynamsoft-barcode-cli cd dynamsoft-barcode-cli -
Configure
Cargo.tomlwith modern dependencies for a robust CLI application:[package] name = "dynamsoft-barcode-cli" version = "0.1.0" edition = "2021" description = "Rust command-line barcode scanner using Dynamsoft SDK" authors = ["Your Name <your.email@example.com>"] [dependencies] clap = { version = "4.0", features = ["derive"] } anyhow = "1.0" thiserror = "1.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" image = "0.24" walkdir = "2.0" path-absolutize = "3.0" # For Windows FFI bindings to Dynamsoft SDK [target.'cfg(windows)'.dependencies] windows = { version = "0.52", features = [ "Win32_Foundation", "Win32_System_LibraryLoader", ] } [build-dependencies] cc = "1.0" walkdir = "2.0" [[bin]] name = "barcode-scanner" path = "src/main.rs"Key Dependencies Explained:
clap: Modern command-line argument parsing with derive macrosanyhow&thiserror: Robust error handling and propagationserde: Serialization support for configuration filesimage: Image processing utilities and format supportcc: Cross-platform C/C++ compiler integration
Step 2: Configuring the Dynamsoft Capture Vision SDK
-
Download and extract the Dynamsoft Capture Vision SDK to create the following directory structure:
examples/rust/ ├── Cargo.toml ├── build.rs ├── src/ │ ├── main.rs │ └── bindings.rs ├── lib/ │ ├── bridge.cpp │ └── bridge.h └── ../../dcv/ ├── include/ │ ├── DynamsoftCaptureVisionRouter.h │ ├── DynamsoftBarcodeReader.h │ ├── DynamsoftCore.h │ ├── DynamsoftLicense.h │ └── DynamsoftUtility.h ├── lib/ │ ├── win/ │ │ ├── *.dll │ │ └── *.lib │ ├── linux/x64/ │ │ └── *.so │ └── mac/ │ └── *.dylib └── resource/ ├── Templates/ └── Models/ -
Create the FFI bridge structure in the
libdirectory:The bridge code serves as the interface between Rust and the C++ Capture Vision SDK, providing a clean C-style API that Rust can easily interact with through FFI.
-
Configure the build system in
build.rsto handle cross-platform compilation:use std::env; use cc::Build; use std::fs; use walkdir::WalkDir; use std::path::{Path, PathBuf}; fn main() { // Determine the target operating system let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap(); println!("cargo:warning=OS: {}", target_os); match target_os.as_str() { "windows" => { // Link Dynamsoft Capture Vision Router for Windows println!("cargo:rustc-link-search=../../dcv/lib/win"); println!("cargo:rustc-link-lib=static=DynamsoftCaptureVisionRouterx64"); println!("cargo:rustc-link-lib=static=DynamsoftBarcodeReaderx64"); println!("cargo:rustc-link-lib=static=DynamsoftCorex64"); println!("cargo:rustc-link-lib=static=DynamsoftLicensex64"); println!("cargo:rustc-link-lib=static=DynamsoftUtilityx64"); // Copy DLL files and resources let src_dir = Path::new("../../dcv/lib/win"); copy_shared_libs_from_dir_to_out_dir(src_dir, &get_out_dir(), "dll"); copy_resource_directories(&get_out_dir()); }, "linux" => { // Link for Linux println!("cargo:rustc-link-search=../../dcv/lib/linux/x64"); println!("cargo:rustc-link-lib=dylib=DynamsoftCaptureVisionRouter"); println!("cargo:rustc-link-lib=dylib=DynamsoftBarcodeReader"); println!("cargo:rustc-link-lib=dylib=DynamsoftCore"); println!("cargo:rustc-link-lib=dylib=DynamsoftLicense"); println!("cargo:rustc-link-lib=dylib=DynamsoftUtility"); // Set rpath and copy files println!("cargo:rustc-link-arg=-Wl,-rpath,../../dcv/lib/linux/x64"); let src_dir = Path::new("../../dcv/lib/linux/x64"); copy_shared_libs_from_dir_to_out_dir(src_dir, &get_out_dir(), "so"); copy_resource_directories(&get_out_dir()); }, "macos" => { // Similar configuration for macOS println!("cargo:rustc-link-search=../../dcv/lib/mac"); // ... macOS-specific linking copy_resource_directories(&get_out_dir()); }, _ => panic!("Unsupported target OS: {}", target_os), } // Compile the C++ bridge code Build::new() .cpp(true) .include("../../dcv/include") .file("lib/bridge.cpp") .compile("bridge"); println!("cargo:rustc-link-lib=static=bridge"); println!("cargo:rustc-link-search=native={}", env::var("OUT_DIR").unwrap()); }Critical Addition: Resource Directory Copying
The build script now includes a crucial function to copy the SDK’s resource directories (
TemplatesandModels) to the output directory. These resources are essential for the Capture Vision SDK to function properly:fn copy_resource_directories(out_dir: &Path) { // Copy Templates directory let templates_src = Path::new("../../dcv/resource/Templates"); let templates_dest = out_dir.join("Templates"); if templates_src.exists() { if templates_dest.exists() { let _ = fs::remove_dir_all(&templates_dest); } copy_dir_recursive(templates_src, &templates_dest) .expect("Failed to copy Templates directory"); println!("Copied Templates directory to {}", templates_dest.display()); } // Copy Models directory let models_src = Path::new("../../dcv/resource/Models"); let models_dest = out_dir.join("Models"); if models_src.exists() { if models_dest.exists() { let _ = fs::remove_dir_all(&models_dest); } copy_dir_recursive(models_src, &models_dest) .expect("Failed to copy Models directory"); println!("Copied Models directory to {}", models_dest.display()); } }
Step 3: Implementing the Modern C++ Bridge Code
The bridge code has been updated to use the latest Capture Vision Router API instead of the deprecated Barcode Reader API. This provides better performance and future-proofing.
Declaring Structures and Functions in bridge.h
Create lib/bridge.h with the FFI interface declarations:
#ifndef BRIDGE_H #define BRIDGE_H #ifdef __cplusplus extern "C" { #endif typedef struct { const char *barcode_type; const char *barcode_value; int x1; int y1; int x2; int y2; int x3; int y3; int x4; int y4; } Barcode; typedef struct { Barcode *barcodes; int count; } BarcodeResults; // Core API functions Barcode *create_barcode(const char *type, const char *value, int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4); BarcodeResults *decode_barcode_file(void *instance, const char *filename); void free_barcode(BarcodeResults *results); int init_license(const char *license); // Capture Vision Router instance management void* CVR_CreateInstance(); void CVR_DestroyInstance(void* instance); #ifdef __cplusplus } #endif #endif // BRIDGE_H Implementing the Bridge Functions in bridge.cpp
Create lib/bridge.cpp with the modern Capture Vision Router implementation:
#include "bridge.h" #include <cstring> #include <cstdlib> #include <stdio.h> #include "DynamsoftCaptureVisionRouter.h" #include "DynamsoftUtility.h" using namespace std; using namespace dynamsoft::license; using namespace dynamsoft::cvr; using namespace dynamsoft::dbr; using namespace dynamsoft::utility; using namespace dynamsoft::basic_structures; Barcode *create_barcode(const char *type, const char *value, int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4) { Barcode *barcode = (Barcode *)std::malloc(sizeof(Barcode)); barcode->barcode_type = _strdup(type); // Use _strdup for Windows compatibility barcode->barcode_value = _strdup(value); barcode->x1 = x1; barcode->y1 = y1; barcode->x2 = x2; barcode->y2 = y2; barcode->x3 = x3; barcode->y3 = y3; barcode->x4 = x4; barcode->y4 = y4; return barcode; } void free_barcode(BarcodeResults *results) { if (!results) return; for (int i = 0; i < results->count; i++) { std::free((void *)results->barcodes[i].barcode_type); std::free((void *)results->barcodes[i].barcode_value); } std::free(results->barcodes); std::free(results); } int init_license(const char *license) { char szErrorMsg[256]; int iRet = CLicenseManager::InitLicense(license, szErrorMsg, 256); if (iRet != EC_OK) { printf("License initialization failed: %s\n", szErrorMsg); } return iRet; } BarcodeResults *decode_barcode_file(void *instance, const char *filename) { CCaptureVisionRouter *cvr = (CCaptureVisionRouter *)instance; BarcodeResults *all_barcodes = NULL; // Use the modern Capture Vision Router API CCapturedResult *result = cvr->Capture(filename, CPresetTemplate::PT_READ_BARCODES); if (result && result->GetErrorCode() == 0) { CDecodedBarcodesResult *barcodeResult = result->GetDecodedBarcodesResult(); if (barcodeResult) { int count = barcodeResult->GetItemsCount(); if (count > 0) { all_barcodes = (BarcodeResults *)std::malloc(sizeof(BarcodeResults)); all_barcodes->count = count; all_barcodes->barcodes = (Barcode *)std::malloc(sizeof(Barcode) * count); for (int i = 0; i < count; i++) { const CBarcodeResultItem *barcodeItem = barcodeResult->GetItem(i); if (barcodeItem) { CQuadrilateral location = barcodeItem->GetLocation(); CPoint *points = location.points; const char *text = barcodeItem->GetText(); const char *format = barcodeItem->GetFormatString(); // Access coordinates using the modern API Barcode *barcode = create_barcode( format ? format : "Unknown", text ? text : "", (int)points[0][0], (int)points[0][1], // Top-left (int)points[1][0], (int)points[1][1], // Top-right (int)points[2][0], (int)points[2][1], // Bottom-right (int)points[3][0], (int)points[3][1] // Bottom-left ); all_barcodes->barcodes[i] = *barcode; std::free(barcode); } } } } } if (result) { result->Release(); } return all_barcodes; } // Modern Capture Vision Router instance management void* CVR_CreateInstance() { return new CCaptureVisionRouter(); } void CVR_DestroyInstance(void* instance) { if (instance) { delete (CCaptureVisionRouter*)instance; } } Key improvements in the modern implementation:
- Uses
CCaptureVisionRouterinstead of the deprecatedDBR_*functions - Proper coordinate access using
points[i][0]andpoints[i][1] - Better error handling and resource management
- Support for the latest SDK features and templates
Step 4: Creating Rust FFI Bindings
Create src/bindings.rs with the Rust FFI interface that matches our C++ bridge:
use std::ffi::c_void; use std::os::raw::c_char; use std::os::raw::c_int; #[repr(C)] pub struct Barcode { pub barcode_type: *const c_char, pub barcode_value: *const c_char, pub x1: c_int, pub y1: c_int, pub x2: c_int, pub y2: c_int, pub x3: c_int, pub y3: c_int, pub x4: c_int, pub y4: c_int, } #[repr(C)] pub struct BarcodeResults { pub barcodes: *mut Barcode, pub count: c_int, } extern "C" { // Bridge functions for barcode operations pub fn free_barcode(barcode: *mut BarcodeResults); pub fn init_license(license: *const c_char) -> c_int; pub fn decode_barcode_file(instance: *mut c_void, filename: *const c_char) -> *mut BarcodeResults; // Modern Capture Vision Router instance management pub fn CVR_CreateInstance() -> *mut c_void; pub fn CVR_DestroyInstance(instance: *mut c_void); } Key Changes from Legacy Implementation:
- Updated function names to use
CVR_*prefix for Capture Vision Router - Simplified interface focusing on essential barcode operations
- Proper memory management with clear ownership semantics
Step 5: Implementing the Rust Application
Create a comprehensive command-line application in src/main.rs with both interactive and batch processing modes:
mod bindings; use std::io::{self, Write}; use std::ffi::CString; use std::path::Path; use std::env; use bindings::*; fn main() { // Check for command line arguments let args: Vec<String> = env::args().collect(); if args.len() > 1 { // Non-interactive mode - process file(s) directly for file_path in &args[1..] { process_file_non_interactive(file_path); } } else { // Interactive mode run_interactive_mode(); } } fn run_interactive_mode() { println!("*************************************************"); println!("Welcome to Dynamsoft Barcode Demo (Rust Version)"); println!("*************************************************"); println!("Hints: Please input 'Q' or 'q' to quit the application."); // Initialize license with your trial key let license = "DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ=="; let ret = unsafe { let license_cstr = CString::new(license).expect("CString::new failed"); init_license(license_cstr.as_ptr()) }; if ret != 0 { println!("License initialization failed with code: {}", ret); return; } // Create Capture Vision Router instance let reader_ptr = unsafe { CVR_CreateInstance() }; if reader_ptr.is_null() { println!("Failed to create barcode reader instance"); return; } // Main processing loop loop { print!("\n>> Step 1: Input your image file's full path:\n"); io::stdout().flush().unwrap(); let mut input = String::new(); io::stdin().read_line(&mut input).expect("Failed to read line"); // Clean and validate input let input = input.trim() .trim_start_matches([' ', '\t', '\n', '\r', '"', '\'']) .trim_end_matches([' ', '\t', '\n', '\r', '"', '\'']); // Check exit conditions if input.eq_ignore_ascii_case("q") { break; } if input.is_empty() { continue; } // Validate file exists let path = Path::new(input); if !path.exists() { println!("Please input a valid path."); continue; } // Process the file process_image(reader_ptr, input); } // Cleanup unsafe { CVR_DestroyInstance(reader_ptr); } } fn process_file_non_interactive(file_path: &str) { // Initialize license for batch processing let license = "DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ=="; let ret = unsafe { let license_cstr = CString::new(license).expect("CString::new failed"); init_license(license_cstr.as_ptr()) }; if ret != 0 { eprintln!("License initialization failed with code: {}", ret); return; } // Create instance for batch processing let reader_ptr = unsafe { CVR_CreateInstance() }; if reader_ptr.is_null() { eprintln!("Failed to create barcode reader instance"); return; } // Validate and process file let path = Path::new(file_path); if !path.exists() { eprintln!("File not found: {}", file_path); unsafe { CVR_DestroyInstance(reader_ptr); } return; } process_image(reader_ptr, file_path); // Cleanup unsafe { CVR_DestroyInstance(reader_ptr); } } fn process_image(reader_ptr: *mut std::ffi::c_void, file_path: &str) { println!("Processing file: {}", file_path); let path_cstr = match CString::new(file_path) { Ok(cstr) => cstr, Err(e) => { println!("Invalid file path: {}", e); return; } }; unsafe { let results_ptr = decode_barcode_file(reader_ptr, path_cstr.as_ptr()); if results_ptr.is_null() { println!("No barcode found."); } else { let results = &*results_ptr; if results.count == 0 { println!("No barcode found."); } else { let barcodes = std::slice::from_raw_parts(results.barcodes, results.count as usize); println!("Decoded {} barcodes", results.count); for (i, barcode) in barcodes.iter().enumerate() { let barcode_format = std::ffi::CStr::from_ptr(barcode.barcode_type) .to_string_lossy(); let barcode_text = std::ffi::CStr::from_ptr(barcode.barcode_value) .to_string_lossy(); println!("Result {}", i + 1); println!("Barcode Format: {}", barcode_format); println!("Barcode Text: {}", barcode_text); println!("Point 1: ({}, {})", barcode.x1, barcode.y1); println!("Point 2: ({}, {})", barcode.x2, barcode.y2); println!("Point 3: ({}, {})", barcode.x3, barcode.y3); println!("Point 4: ({}, {})", barcode.x4, barcode.y4); } } free_barcode(results_ptr); } } println!(); } 
Source Code
https://github.com/yushulx/cmake-cpp-barcode-qrcode-mrz/tree/main/examples/rust