Small hashing library in Zig with a runtime algorithm enum, explicit Zig 0.16 I/O, cancellable hash requests, and an optional C ABI.
- Pre-1.0 API may change.
- Minimum Zig version:
0.16.0(build.zig.zon). - Current C ABI version:
ZFH_API_VERSION = 4.
- Unified algorithm selection via
HashAlgorithm - Hashing for in-memory data and files
- Explicit
std.Iofor file operations - High-level streaming API via
HashStream - Low-level algorithm state via
RuntimeHasher - Caller-managed output buffers
- Optional C ABI for native/FFI integrations
- Cooperative cancellation through
Operation - Keyed/seeded modes where applicable
- Optional mmap fast path for stable regular files
Add dependency to your Zig project:
zig fetch --save https://github.com/Preeternal/zig-files-hash/archive/refs/tags/v<VERSION>.tar.gzThen wire module import in your build.zig:
const zfh_dep = b.dependency("zig_files_hash", .{
.target = target,
.optimize = optimize,
});
exe.root_module.addImport("zig_files_hash", zfh_dep.module("zig_files_hash"));pub const HashAlgorithm
pub const HashOptions
pub const HashRequest
pub const Operation
pub const HashStream
pub const RuntimeHasher
pub const Context
pub const Error
pub const max_digest_length
pub fn digestLength(alg: HashAlgorithm) usizeRecommended API choice:
- Use
HashStreamwhen data already arrives in chunks, when the caller owns the file reading pipeline, or when integration code needs cancellation between chunks. - Use
Context.fileHash/Context.fileHashInDirfor simple one-shot file hashing in Zig code. - Use
stringHashfor small in-memory inputs. - Use
RuntimeHasheronly when you need the low-level algorithm state directly.
One-shot APIs:
pub fn stringHash(
alg: HashAlgorithm,
data: []const u8,
out: []u8,
request: ?HashRequest,
) !usize
pub fn fileHash(
io: std.Io,
alg: HashAlgorithm,
path: []const u8,
out: []u8,
request: ?HashRequest,
) !usize
pub fn fileHashInDir(
io: std.Io,
alg: HashAlgorithm,
dir: std.Io.Dir,
sub_path: []const u8,
out: []u8,
request: ?HashRequest,
) !usize
// POSIX only.
pub fn fdHash(
alg: HashAlgorithm,
fd: std.posix.fd_t,
out: []u8,
request: ?HashRequest,
) !usizefdHash is POSIX-only. Use it when the caller already owns an open file
descriptor and cannot reliably provide a filesystem path. It reads from the
current fd position, keeps the read loop inside Zig, and does not close the fd.
Context stores a reusable std.Io value:
pub fn Context.init(io: std.Io) Context
pub fn Context.fileHash(...)
pub fn Context.fileHashInDir(...)Context.fileHash resolves relative paths from std.Io.Dir.cwd(). Use
Context.fileHashInDir when the caller should choose the base directory
explicitly.
Streaming APIs:
pub fn HashStream.init(alg: HashAlgorithm, request: ?HashRequest) !HashStream
pub fn HashStream.update(self: *HashStream, chunk: []const u8) !void
pub fn HashStream.digestLength(self: *const HashStream) usize
pub fn HashStream.final(self: *HashStream, out: []u8) !usize
pub fn HashStream.finalResult(self: *HashStream) !RuntimeHasher.DigestRuntimeHasher remains public as a lower-level primitive without request/cancel
handling:
pub fn RuntimeHasher.init(alg: HashAlgorithm, options: ?HashOptions) !RuntimeHasher
pub fn RuntimeHasher.update(self: *RuntimeHasher, chunk: []const u8) void
pub fn RuntimeHasher.digestLength(self: *const RuntimeHasher) usize
pub fn RuntimeHasher.final(self: *RuntimeHasher, out: []u8) !usize
pub fn RuntimeHasher.finalResult(self: *RuntimeHasher) RuntimeHasher.Digest
pub fn RuntimeHasher.Digest.slice(self: *const RuntimeHasher.Digest) []const u8Return value is digest length in bytes. Digest bytes are written to out[0..len].
finalResult returns an owned fixed-size digest wrapper; consume bytes via
digest.slice().
pub const HashAlgorithm = enum {
@"SHA-224",
@"SHA-256",
@"SHA-384",
@"SHA-512",
@"SHA-512/224",
@"SHA-512/256",
MD5,
@"SHA-1",
@"XXH3-64",
BLAKE3,
@"HMAC-SHA-224",
@"HMAC-SHA-256",
@"HMAC-SHA-384",
@"HMAC-SHA-512",
@"HMAC-MD5",
@"HMAC-SHA-1",
};pub const HashOptions = struct {
seed: ?u64 = null,
key: ?[]const u8 = null,
};
pub const HashRequest = struct {
hash_options: ?HashOptions = null,
operation: ?*const Operation = null,
use_mmap: bool = false,
};hash_options affects the digest
operation affects execution only and is used for cooperative cancellation.
use_mmap only changes file I/O for fileHash / fileHashInDir. It is off by
default; enable it only for stable regular files after benchmarking your
workload and accepting mmap-specific risks. Local benchmarks ranged from
slightly slower to about 20% faster, usually only a few percent.
var op = zfh.Operation.init();
var stream = try zfh.HashStream.init(.BLAKE3, .{
.operation = &op,
});
op.cancel();
try stream.update("data"); // returns error.OperationCanceledThe Operation object must outlive every HashRequest / HashStream that
stores a pointer to it.
pub const Error = error{
KeyRequired,
InvalidKeyLength,
OutputBufferTooSmall,
OperationCanceled,
InvalidState,
};Rules:
HMAC-*:keyis required, otherwiseerror.KeyRequiredBLAKE3: keyed mode requireskey.len == 32, otherwiseerror.InvalidKeyLengthXXH3-64: optionalseed- SHA/MD5 algorithms ignore options
- If
outis too small:error.OutputBufferTooSmall - If
operationwas canceled:error.OperationCanceled - If a
HashStreamis used after finalization:error.InvalidState
fileHash / fileHashInDir / fdHash return library errors plus filesystem/OS
errors from opening and reading files. The exact filesystem error set is
platform-dependent.
- API returns raw digest bytes, not hex string.
- Print hex with
{x}in Zig or your own hex encoder in C. XXH3-64bytes are written in canonical big-endian order.XXH3-64is non-cryptographic (fast checksum/hash, not for security).
const std = @import("std");
const zfh = @import("zig_files_hash");
pub fn main() !void {
var out: [zfh.max_digest_length]u8 = undefined;
const len = try zfh.stringHash(.@"SHA-256", "hello world", out[0..], null);
std.debug.print("SHA-256 = {x}\n", .{out[0..len]});
}const std = @import("std");
const zfh = @import("zig_files_hash");
pub fn main(init: std.process.Init) !void {
const context = zfh.Context.init(init.io);
const path: []const u8 = "file.bin";
var out: [zfh.max_digest_length]u8 = undefined;
const len = try context.fileHash(.BLAKE3, path, out[0..], .{
.hash_options = .{
.key = "0123456789abcdef0123456789abcdef",
},
});
std.debug.print("BLAKE3 = {x}\n", .{out[0..len]});
}fdHash is POSIX-only. Use it when a platform API already returned an open fd,
for example for an Android content:// provider or a provider-backed URL that
cannot be represented as a regular filesystem path.
const std = @import("std");
const zfh = @import("zig_files_hash");
pub fn main() !void {
const fd = try std.posix.openat(
std.posix.AT.FDCWD,
"file.bin",
.{ .ACCMODE = .RDONLY },
0,
);
defer _ = std.posix.system.close(fd);
var out: [zfh.max_digest_length]u8 = undefined;
const len = try zfh.fdHash(.BLAKE3, fd, out[0..], null);
std.debug.print("BLAKE3 = {x}\n", .{out[0..len]});
}The function reads from the descriptor's current position, does not close it, and hashes the input in chunks.
const std = @import("std");
const zfh = @import("zig_files_hash");
pub fn main(init: std.process.Init) !void {
const io = init.io;
const context = zfh.Context.init(io);
var out: [zfh.max_digest_length]u8 = undefined;
var dir = try std.Io.Dir.cwd().openDir(io, "fixtures", .{});
defer dir.close(io);
const len = try context.fileHashInDir(.@"SHA-256", dir, "sample.bin", out[0..], null);
std.debug.print("SHA-256 = {x}\n", .{out[0..len]});
}const std = @import("std");
const zfh = @import("zig_files_hash");
pub fn main() !void {
var out: [zfh.max_digest_length]u8 = undefined;
var stream = try zfh.HashStream.init(.@"SHA-256", null);
try stream.update("hello ");
try stream.update("world");
const len = try stream.final(out[0..]);
std.debug.print("SHA-256 = {x}\n", .{out[0..len]});
}const zfh = @import("zig_files_hash");
pub fn example() !void {
var op = zfh.Operation.init();
var stream = try zfh.HashStream.init(.BLAKE3, .{ .operation = &op });
op.cancel();
try stream.update("chunk"); // error.OperationCanceled
}The C ABI lives in src/c_api.zig, with headers:
src/zig_files_hash_c_api.hsrc/zig_files_hash_c_api_generated.h
Build artifacts:
zig build c-api-static # .a / .lib
zig build c-api-shared # .dylib / .so / .dll
zig build c-api-header # installs headers to zig-out/include
zig build c-api # all of the aboveInstalled outputs:
zig-out/lib/libzig_files_hash_c_api_static.a(or.lib)zig-out/lib/libzig_files_hash_c_api.dylib/.so/.dllzig-out/include/zig_files_hash_c_api.hzig-out/include/zig_files_hash_c_api_generated.h
For native integrations that already read files in chunks, prefer the streaming
hasher functions (zfh_hasher_init_inplace, zfh_hasher_update,
zfh_hasher_final). Use zfh_context_file_hash as a convenience one-shot API
when C is responsible for opening and reading the file.
Main C functions:
size_t zfh_max_digest_length(void);
uint32_t zfh_api_version(void);
zfh_error zfh_digest_length(zfh_algorithm alg, size_t *out_len_ptr);
const char *zfh_error_message(zfh_error code);
zfh_error zfh_string_hash(...);
zfh_error zfh_context_create(zfh_context **out_ctx);
zfh_error zfh_context_destroy(zfh_context *ctx);
zfh_error zfh_context_file_hash(...);
zfh_error zfh_fd_hash(...); /* POSIX only */
size_t zfh_operation_state_size(void);
size_t zfh_operation_state_align(void);
zfh_error zfh_operation_init_inplace(void *operation_ptr, size_t operation_len);
zfh_error zfh_operation_cancel(void *operation_ptr, size_t operation_len);
size_t zfh_hasher_state_size(void);
size_t zfh_hasher_state_align(void);
zfh_error zfh_hasher_init_inplace(...);
zfh_error zfh_hasher_update(...);
zfh_error zfh_hasher_final(...);zfh_api_version() returns the same value as ZFH_API_VERSION.
Options and cancellation are passed through zfh_request:
typedef struct zfh_options {
uint32_t struct_size;
uint32_t flags;
uint64_t seed;
const uint8_t *key_ptr;
size_t key_len;
} zfh_options;
typedef struct zfh_request {
uint32_t struct_size;
const zfh_options *options_ptr;
void *operation_ptr;
size_t operation_len;
} zfh_request;Use flags:
ZFH_OPTION_HAS_SEEDZFH_OPTION_HAS_KEYZFH_OPTION_USE_MMAPfor path-based file hashing
Set struct_size with ZFH_OPTIONS_STRUCT_SIZE and
ZFH_REQUEST_STRUCT_SIZE. If no options or cancellation are needed, pass
NULL for request_ptr. Unknown option flags are rejected with
ZFH_INVALID_ARGUMENT.
Operation state is caller-provided memory:
size_t op_size = zfh_operation_state_size();
size_t op_align = zfh_operation_state_align();Allocate a buffer with at least op_size bytes and op_align alignment, then:
zfh_operation_init_inplace(op_ptr, op_size);
zfh_request req = {
.struct_size = ZFH_REQUEST_STRUCT_SIZE,
.options_ptr = NULL,
.operation_ptr = op_ptr,
.operation_len = op_size,
};
zfh_operation_cancel(op_ptr, op_size);zfh_string_hash, zfh_context_file_hash, zfh_fd_hash, zfh_hasher_update, and
zfh_hasher_final can return ZFH_OPERATION_CANCELED when the operation is
canceled.
File hashing in C uses an explicit context:
zfh_context *ctx = NULL;
zfh_context_create(&ctx);
zfh_context_file_hash(
ctx,
ZFH_ALG_SHA_256,
path_ptr,
path_len,
request_ptr,
out_ptr,
out_len,
&written_len
);
zfh_context_destroy(ctx);path_ptr is byte data with explicit path_len; null-termination is not
required.
Set ZFH_OPTION_USE_MMAP in zfh_options.flags to opt into mmap for this path
API. It is disabled by default and should be used only for stable regular files
after benchmarking the workload.
On POSIX platforms, callers that already own an open descriptor can avoid duplicating the read loop in C or a wrapper:
zfh_fd_hash(
ZFH_ALG_SHA_256,
fd,
request_ptr,
out_ptr,
out_len,
&written_len
);The function reads from the descriptor's current position, does not close it, and hashes the input in chunks.
- Query state requirements with
zfh_hasher_state_sizeandzfh_hasher_state_align. - Caller provides an aligned state buffer (
state_ptr,state_len) tozfh_hasher_init_inplace. zfh_hasher_init_inplacetakesconst zfh_request *request_ptr.- Call
zfh_hasher_updateany number of times with chunked data. - Call
zfh_hasher_finalonce to write digest bytes. - After successful
zfh_hasher_final, furtherzfh_hasher_update/zfh_hasher_finalcalls on the same state returnZFH_INVALID_ARGUMENT. - If the request operation is canceled,
zfh_hasher_update/zfh_hasher_finalreturnZFH_OPERATION_CANCELED.
ZFH_OKZFH_INVALID_ARGUMENTZFH_INVALID_ALGORITHMZFH_KEY_REQUIREDZFH_INVALID_KEY_LENGTHZFH_OUTPUT_BUFFER_TOO_SMALLZFH_OPERATION_CANCELEDZFH_INVALID_STATEZFH_FILE_NOT_FOUNDZFH_ACCESS_DENIEDZFH_INVALID_PATHZFH_IO_ERRORZFH_UNKNOWN_ERROR
Output length pointers are set to 0 on error.
zig build
zig build testgetDemoOptionsArrayexists for local demo/testing flows; treat it as a non-stable helper.- On AArch64,
SHA-224/SHA-256and related HMAC variants can be much faster with CPUsha2extensions. - For portable builds, keep generic targets. For controlled hardware or benchmarks, compare with
-Dcpu=baseline+sha2.
MIT. See LICENSE.