The i24 crate provides specialized integer types for Rust: i24 (24-bit signed) and u24 (24-bit unsigned). These types fill precision gaps in Rust's integer types and are particularly useful in audio processing, embedded systems, network protocols, and other scenarios where specific bit-width precision is required.
- i24: 24-bit signed integer (range: -8,388,608 to 8,388,607)
- u24: 24-bit unsigned integer (range: 0 to 16,777,215)
- Seamless conversion to/from standard Rust integer types
- Complete arithmetic operations with overflow checking
- Bitwise operations
- Conversions from various byte representations (little-endian, big-endian, native)
- Wire/packed format support for binary protocols
- Compile-time macros:
i24!()andu24!()for checked construction - Implements standard traits:
Debug,Display,PartialEq,Eq,PartialOrd,Ord,Hash
This crate came about as a part of the Wavers project, which is a Wav file reader and writer for Rust.
All integer types have Python bindings available via the pyo3 feature.
Add this to your`` Cargo.toml`:
[dependencies]
i24 = "2.2.0"use i24::{i24, u24};
// Using macros for compile-time checked construction
let signed_24 = i24!(1000);
let unsigned_24 = u24!(2000);
// Arithmetic operations
let sum_24 = signed_24 + i24!(500);
assert_eq!(sum_24.to_i32(), 1500);
// Conversions
let as_i32: i32 = signed_24.into();
let as_u32: u32 = unsigned_24.into();The i24!() and u24!() macros allow you to create values at compile time, ensuring they're within the valid range.
For working with binary data, all types support reading/writing from byte arrays:
use i24::{i24, u24};
// Reading from bytes (little-endian, big-endian, native)
let bytes_le = [0x00, 0x01, 0x02]; // 3-byte representation
let value = i24::from_le_bytes(bytes_le);
// Writing to bytes
let bytes: [u8; 3] = value.to_be_bytes();
// Bulk operations for slices
let raw_data: &[u8] = &[0x00, 0x01, 0x02, 0x00, 0x01, 0xFF];
let values: Vec<i24> = i24::read_i24s_be(raw_data).expect("valid buffer");
let encoded: Vec<u8> = i24::write_i24s_be(&values);For binary protocols and serialization, use the PackedStruct trait:
use i24::{i24, packed::PackedStruct};
#[derive(Debug, Clone, PartialEq)]
struct WireFormat {
header: u32,
samples: [i24; 5],
}
impl PackedStruct for WireFormat {
const PACKED_SIZE: usize = 4 + 5 * 3; // u32 + 5 * i24 = 19 bytes
fn from_packed_bytes(bytes: &[u8]) -> Option<Self> {
// Implementation for deserializing from bytes
// ...
}
fn to_packed_bytes(&self) -> Vec<u8> {
// Implementation for serializing to bytes
// ...
}
}All integer types strive to behave similarly to Rust's built-in integer types, with some important considerations:
- i24: [-8,388,608, 8,388,607]
- u24: [0, 16,777,215]
- Arithmetic operations match the behavior of their closest standard Rust integer type
- Bitwise operations are performed on the actual bit-width representation
- Always use checked arithmetic operations when dealing with untrusted input
All types align with bytemuck safety requirements (NoUninit, Zeroable, AnyBitPattern), ensuring safe byte-to-value conversions. The bulk I/O operations use bytemuck::cast_slice internally for efficient, safe conversions.
- pyo3: Enables Python bindings for all integer types (i24, u24)
- serde: Enables
SerializeandDeserializetraits for all integer types - alloc: Enables bulk I/O operations and
PackedStructfunctionality
Contributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under MIT - see the LICENSE file for details.
The crate was tested using the code found in the i24_benches directory of the repo. Unsurprisingly, the performance of both types matches the performance of the underlying 32-bit type.