Skip to content
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
114 changes: 114 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,120 @@ jobs:
exit 1
fi

c-abi:
# Builds the libzstd-compatible C ABI front end and verifies it is a real
# drop-in: vendored headers match upstream verbatim, the cdylib advertises
# SONAME libzstd.so.1, every declared symbol is exported, and a genuine C
# consumer links + round-trips through the vendored header.
needs: lint
timeout-minutes: 12
runs-on: ubuntu-latest
env:
UPSTREAM_TAG: v1.5.7
steps:
- uses: actions/checkout@v6
with:
# Build + test + header-diff job (no push); don't leave the token
# in the checkout's git config.
persist-credentials: false
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: Swatinem/rust-cache@v2
with:
prefix-key: c-abi
- name: Clippy (c-api)
run: cargo clippy -p structured-zstd-c --all-targets -- -D warnings
- name: Unit + ABI tests
run: cargo test -p structured-zstd-c
- name: Vendored headers match upstream verbatim
# The headers are copied byte-for-byte from the pinned upstream tag; a
# diff means someone edited a vendored header (forbidden) or the pin
# moved without re-vendoring.
run: |
set -euo pipefail
base="https://github.com/facebook/zstd/${UPSTREAM_TAG}/lib"
for h in zstd.h zdict.h zstd_errors.h; do
curl --retry 5 --retry-all-errors --retry-delay 2 -fsSL "${base}/${h}" -o "/tmp/${h}.upstream"
if ! diff -u "/tmp/${h}.upstream" "c-api/include/${h}"; then
echo "::error::c-api/include/${h} diverges from upstream ${UPSTREAM_TAG}"
exit 1
fi
done
echo "vendored headers identical to upstream ${UPSTREAM_TAG}"
- name: Build cdylib + staticlib
run: cargo build -p structured-zstd-c
- name: SONAME is libzstd.so.1
run: |
set -euo pipefail
so=target/debug/libstructured_zstd.so
soname=$(readelf -d "$so" | sed -n 's/.*SONAME.*\[\(.*\)\]/\1/p')
echo "SONAME=$soname"
test "$soname" = "libzstd.so.1"
- name: All declared symbols are exported
run: |
set -euo pipefail
so=target/debug/libstructured_zstd.so
exported=$(nm -D --defined-only "$so" | awk '{print $NF}')
missing=0
for sym in \
ZSTD_compress ZSTD_decompress ZSTD_compressBound \
ZSTD_getFrameContentSize ZSTD_findFrameCompressedSize \
ZSTD_isError ZSTD_getErrorCode ZSTD_getErrorName ZSTD_getErrorString \
ZSTD_minCLevel ZSTD_maxCLevel ZSTD_defaultCLevel \
ZSTD_versionNumber ZSTD_versionString \
ZSTD_createCCtx ZSTD_freeCCtx ZSTD_createDCtx ZSTD_freeDCtx \
ZSTD_compressCCtx ZSTD_decompressDCtx ZSTD_sizeof_CCtx ZSTD_sizeof_DCtx \
ZSTD_frameHeaderSize ZSTD_getFrameHeader ZSTD_getFrameHeader_advanced \
ZSTD_findDecompressedSize ZSTD_decompressBound \
ZDICT_trainFromBuffer ZDICT_finalizeDictionary ZDICT_getDictID \
ZDICT_getDictHeaderSize ZDICT_isError ZDICT_getErrorName; do
if ! grep -qx "$sym" <<<"$exported"; then
echo "::error::symbol $sym not exported from $so"
missing=1
fi
done
test "$missing" -eq 0
- name: pkg-config reports upstream version
run: |
set -euo pipefail
# Validate every discovered libzstd.pc, not just the first hit: a
# restored cache can leave a stale file ahead of the fresh build.
mapfile -t pcs < <(find target -type f -name libzstd.pc)
test "${#pcs[@]}" -gt 0
for pc in "${pcs[@]}"; do
echo "checking $pc"; cat "$pc"
grep -qx "Version: 1.5.7" "$pc"
done
- name: Real C consumer links + round-trips
run: |
set -euo pipefail
# The cdylib advertises SONAME libzstd.so.1, so a consumer records a
# NEEDED dependency on that name; provide it via a symlink. Also
# expose the canonical `libzstd.so` link name and link with `-lzstd`,
# exactly the path a real drop-in C consumer uses.
ln -sf libstructured_zstd.so target/debug/libzstd.so.1
ln -sf libstructured_zstd.so target/debug/libzstd.so
cc -std=c11 -Wall -Wextra -Ic-api/include c-api/tests/c_consumer.c \
-Ltarget/debug -lzstd -o /tmp/c_consumer
LD_LIBRARY_PATH=target/debug /tmp/c_consumer
- name: musl static drop-in builds + exports symbols
# musl is a std target (not no-std); its default `+crt-static` profile
# makes the static archive `libstructured_zstd.a` the canonical drop-in
# for Alpine / fully-static binaries (the cdylib is dropped under
# crt-static, which is expected). Verify the archive builds and carries
# the exported wrappers.
run: |
set -euo pipefail
rustup target add x86_64-unknown-linux-musl
cargo build -p structured-zstd-c --target x86_64-unknown-linux-musl
a=target/x86_64-unknown-linux-musl/debug/libstructured_zstd.a
test -f "$a"
for sym in ZSTD_compress ZSTD_decompress ZSTD_versionNumber ZSTD_getFrameHeader; do
nm "$a" | grep -qE " T ${sym}$" || { echo "::error::$sym missing from musl staticlib"; exit 1; }
done
echo "musl staticlib OK ($(du -h "$a" | cut -f1))"

test:
needs: lint
timeout-minutes: 15
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[workspace]
resolver = "3"
members = ["zstd", "cli", "zstd-wasm"]
members = ["zstd", "cli", "zstd-wasm", "c-api"]

# Bench profile inherits from release but overrides LTO/codegen-units so
# the perf measurement reflects full cross-crate inlining. We deliberately
Expand Down
42 changes: 42 additions & 0 deletions c-api/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
[package]
name = "structured-zstd-c"
version = "0.0.33"
rust-version = "1.92"
authors = ["Structured World Foundation <foundation@sw.foundation>"]
edition = "2024"
license = "Apache-2.0"
homepage = "https://github.com/structured-world/structured-zstd"
repository = "https://github.com/structured-world/structured-zstd"
description = "C ABI (libzstd-compatible) front end for the structured-zstd pure-Rust codec."
# The cdylib is consumed as a system library (drop-in for libzstd.so.1), not
# from crates.io, so it stays out of the release-plz publish set for now.
publish = false

[lib]
# The output artifacts are `libstructured_zstd.so` / `.a`; the SONAME is
# rewritten to `libzstd.so.1` by build.rs so a consumer linked against
# upstream libzstd resolves against this build with no rebuild.
name = "structured_zstd"
# `lib` (rlib) is added alongside the C artifacts so the crate's own test
# harness can link the wrappers; the shipped artifacts are the cdylib + staticlib.
crate-type = ["lib", "cdylib", "staticlib"]

# No `[features]` / `std` contract by design: this is a host-only cdylib/
# staticlib whose `extern "C"` wrappers guard the boundary with
# `std::panic::catch_unwind` (a Rust panic must not unwind into C), so the
# crate cannot build without std. A default-on `std` feature would advertise a
# `--no-default-features` build that fails to compile. This deliberately
# deviates from the no-std-library feature convention (which targets crates that
# must support a no_std build); a host-only binding has no such build. The
# no_std + alloc surface lives in the `codec` crate below.

[dependencies]
# The codec is imported under the alias `codec` rather than its own name: this
# lib is itself named `structured_zstd` (so the artifact is `libstructured_zstd.so`),
# and a same-named extern would make rustdoc's doctest harness see two
# `structured_zstd` rlibs (E0464). The alias keeps the artifact name while
# giving the dependency a distinct crate name in this crate.
# Default features pull in std + hash (checksums/LDM) + the per-CPU kernel
# tiers — the C ABI is a host library, so std is always available here.
# `dict_builder` adds the dictionary-training surface backing the ZDICT_* C API.
codec = { path = "../zstd", package = "structured-zstd", features = ["dict_builder"] }
61 changes: 61 additions & 0 deletions c-api/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//! Build glue for the C ABI front end:
//! - emits the SONAME / install-name a consumer of upstream `libzstd`
//! expects, so the cdylib is a true drop-in;
//! - installs the vendored headers under `OUT_DIR/include` for packaging;
//! - generates `libzstd.pc` reporting the vendored upstream version.

use std::env;
use std::fs;
use std::path::PathBuf;

/// Upstream zstd release the vendored headers + reported pkg-config version
/// track. Keep in sync with the tracking comment at the top of each header.
const UPSTREAM_VERSION: &str = "1.5.7";

fn main() {
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();

// A binary linked against upstream `libzstd.so.1` resolves by SONAME, so
// the cdylib must advertise that exact name to be substitutable.
match target_os.as_str() {
"linux" | "android" => {
println!("cargo:rustc-cdylib-link-arg=-Wl,-soname,libzstd.so.1");
}
"macos" | "ios" => {
println!("cargo:rustc-cdylib-link-arg=-Wl,-install_name,libzstd.1.dylib");
}
_ => {}
}

let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR set by cargo"));
let include_src = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("include");

// Stage the vendored headers next to the built library so downstream
// packaging can install them under /usr/include alongside the .so.
let include_dst = out_dir.join("include");
fs::create_dir_all(&include_dst).expect("create OUT_DIR/include");
for header in ["zstd.h", "zdict.h", "zstd_errors.h"] {
fs::copy(include_src.join(header), include_dst.join(header))
.unwrap_or_else(|e| panic!("copy vendored header {header}: {e}"));
println!("cargo:rerun-if-changed=include/{header}");
}

// `pkg-config --modversion libzstd` must report the upstream version a
// consumer's build system pins against.
let pc = format!(
"prefix=/usr\n\
exec_prefix=${{prefix}}\n\
libdir=${{exec_prefix}}/lib\n\
includedir=${{prefix}}/include\n\
\n\
Name: zstd\n\
Description: structured-zstd, a libzstd-compatible pure-Rust zstd codec\n\
URL: https://github.com/structured-world/structured-zstd\n\
Version: {UPSTREAM_VERSION}\n\
Libs: -L${{libdir}} -lzstd\n\
Cflags: -I${{includedir}}\n"
);
fs::write(out_dir.join("libzstd.pc"), pc).expect("write libzstd.pc");

println!("cargo:rerun-if-changed=build.rs");
}
Loading
Loading