Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Build a C-compatible shared library #4

Merged
merged 3 commits into from
Dec 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,9 @@ harness = false

[profile.release]
strip = true

[build-dependencies]
cbindgen = "0.27.0"

[lib]
crate-type = ["lib", "cdylib"]
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ Primarily changes the `CRC-64/XZ` (aka `CRC-64/GO-ECMA`) polynomial from [crc64f

## Usage

### Rust

```rust
use crc64fast_nvme::Digest;

Expand All @@ -32,6 +34,11 @@ let checksum = c.sum64();
assert_eq!(checksum, 0xd9160d1fa8e418e3);
```

### C-compatible shared library
`cargo build` will produce a shared library target (`.so` on Linux, `.dll` on Windows, `.dylib` on macOS, etc) and `crc64vnme.h` header file for use in non-Rust projects, such as through FFI.

There is a [crc-fast-php](https://github.com/awesomized/crc-fast-php) library using it with PHP, for example.

## CLI example
A simple CLI implementation can be found in [crc_64_nvme_checksum.rs](src\bin\crc_64_nvme_checksum.rs), which will calculate the `CRC-64/NVME` checksum for a file on disk.

Expand Down
11 changes: 11 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// build.rs

extern crate cbindgen;

fn main() {
let crate_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();

cbindgen::generate(crate_dir)
.expect("Unable to generate C bindings.")
.write_to_file("crc64nvme.h");
}
36 changes: 36 additions & 0 deletions crc64nvme.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#include <cstdarg>
#include <cstdint>
#include <cstdlib>
#include <ostream>
#include <new>

/// Represents an in-progress CRC-64 computation.
struct Digest;

/// Begin functionality for building a C-compatible library
///
/// Opaque type for C for use in FFI
struct DigestHandle {
Digest *_0;
};

extern "C" {

DigestHandle *digest_new();

/// # Safety
///
/// Uses unsafe method calls
void digest_write(DigestHandle *handle, const char *data, uintptr_t len);

/// # Safety
///
/// Uses unsafe method calls
uint64_t digest_sum64(const DigestHandle *handle);

/// # Safety
///
/// Uses unsafe method calls
void digest_free(DigestHandle *handle);

} // extern "C"
186 changes: 185 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
//! assert_eq!(checksum, 0xd9160d1fa8e418e3);
//! ```

use std::os::raw::c_char;
use std::slice;

mod pclmulqdq;
mod table;

Expand All @@ -30,6 +33,59 @@ pub struct Digest {
state: u64,
}

/// Begin functionality for building a C-compatible library
///
/// Opaque type for C for use in FFI
#[repr(C)]
pub struct DigestHandle(*mut Digest);

#[no_mangle]
pub extern "C" fn digest_new() -> *mut DigestHandle {
let digest = Box::new(Digest::new());
let handle = Box::new(DigestHandle(Box::into_raw(digest)));
Box::into_raw(handle)
}

/// # Safety
///
/// Uses unsafe method calls
#[no_mangle]
pub unsafe extern "C" fn digest_write(handle: *mut DigestHandle, data: *const c_char, len: usize) {
if handle.is_null() || data.is_null() {
return;
}

let digest = &mut *(*handle).0;
let bytes = slice::from_raw_parts(data as *const u8, len);
digest.write(bytes);
}

/// # Safety
///
/// Uses unsafe method calls
#[no_mangle]
pub unsafe extern "C" fn digest_sum64(handle: *const DigestHandle) -> u64 {
if handle.is_null() {
return 0;
}

let digest = &*(*handle).0;
digest.sum64()
}

/// # Safety
///
/// Uses unsafe method calls
#[no_mangle]
pub unsafe extern "C" fn digest_free(handle: *mut DigestHandle) {
if !handle.is_null() {
let handle = Box::from_raw(handle);
let _ = Box::from_raw(handle.0);
}
}

// end C-compatible library

impl Digest {
/// Creates a new `Digest`.
///
Expand Down Expand Up @@ -69,9 +125,10 @@ impl Default for Digest {

#[cfg(test)]
mod tests {
use super::Digest;
use super::*;
use proptest::collection::size_range;
use proptest::prelude::*;
use std::ptr;

// CRC-64/NVME
//
Expand Down Expand Up @@ -177,4 +234,131 @@ mod tests {
prop_assert_eq!(hasher_1.sum64(), hasher_2.sum64());
}
}

// test the FFI Digest functions
#[test]
fn test_ffi_digest_lifecycle() {
unsafe {
// Create new digest
let handle = digest_new();
assert!(!handle.is_null(), "Digest creation failed");

// Write some data
let data = b"hello world!";
digest_write(handle, data.as_ptr() as *const c_char, data.len());

// Get sum and verify against known value
let sum = digest_sum64(handle);
assert_eq!(sum, 0xd9160d1fa8e418e3, "CRC64 calculation incorrect");

// Clean up
digest_free(handle);
}
}

#[test]
fn test_ffi_null_handling() {
unsafe {
// Test null handle with write
digest_write(ptr::null_mut(), b"test".as_ptr() as *const c_char, 4);

// Test null data with valid handle
let handle = digest_new();
digest_write(handle, ptr::null(), 0);

// Test null handle with sum64
let sum = digest_sum64(ptr::null());
assert_eq!(sum, 0, "Null handle should return 0");

// Clean up
digest_free(handle);
}
}

#[test]
fn test_ffi_empty_data() {
unsafe {
let handle = digest_new();

// Write empty data
digest_write(handle, b"".as_ptr() as *const c_char, 0);
let sum = digest_sum64(handle);
assert_eq!(sum, 0, "Empty data should produce 0");

digest_free(handle);
}
}

#[test]
fn test_ffi_binary_data() {
unsafe {
let handle = digest_new();

// Test with binary data including null bytes
let data = [0u8, 1, 2, 3, 0, 4, 5, 0, 6];
digest_write(handle, data.as_ptr() as *const c_char, data.len());

// Write additional data to test streaming
let more_data = [7u8, 8, 9];
digest_write(handle, more_data.as_ptr() as *const c_char, more_data.len());

let sum = digest_sum64(handle);
assert_ne!(sum, 0, "Binary data should produce non-zero CRC");

digest_free(handle);
}
}

#[test]
fn test_ffi_large_vectors() {
unsafe {
let zeros = vec![0u8; 4096];
let ones = vec![255u8; 4096];

let handle = digest_new();
digest_write(handle, zeros.as_ptr() as *const c_char, zeros.len());
let sum = digest_sum64(handle);
assert_eq!(sum, 0x6482d367eb22b64e, "Failed on 4096 zeros");
digest_free(handle);

let handle = digest_new();
digest_write(handle, ones.as_ptr() as *const c_char, ones.len());
let sum = digest_sum64(handle);
assert_eq!(sum, 0xc0ddba7302eca3ac, "Failed on 4096 ones");
digest_free(handle);
}
}

#[test]
fn test_ffi_standard_strings() {
unsafe {
let test_cases: Vec<(&[u8], u64)> = vec![(b"123456789", 0xae8b14860a799888), (b"", 0)];

for (input, expected) in test_cases {
let handle = digest_new();
digest_write(handle, input.as_ptr() as *const c_char, input.len());
let sum = digest_sum64(handle);
assert_eq!(sum, expected, "Failed on test vector: {:?}", input);
digest_free(handle);
}
}
}

#[test]
fn test_ffi_incremental_update() {
unsafe {
let handle = digest_new();

// Write data incrementally
let data = "hello world!";
for byte in data.bytes() {
digest_write(handle, &byte as *const u8 as *const c_char, 1);
}

let sum = digest_sum64(handle);
assert_eq!(sum, 0xd9160d1fa8e418e3, "Incremental update failed");

digest_free(handle);
}
}
}
Loading