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
37 changes: 37 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,40 @@ jobs:
run: cargo test --verbose --all-features
- name: Check formatting
run: cargo fmt -- --check --verbose

# Verify the optional system-linking path: build against a system libnghttp2
# discovered via pkg-config instead of the bundled sources.
system-link:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
submodules: 'recursive'
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@1.92.0
- name: Install system libnghttp2
run: |
sudo apt-get update
sudo apt-get install -y libnghttp2-dev pkg-config
pkg-config --modversion libnghttp2
- name: Build against system libnghttp2 (feature)
run: cargo build --verbose --features system
- name: Test against system libnghttp2 (env var)
env:
LIBNGHTTP2_SYS_USE_PKG_CONFIG: "1"
run: cargo test --verbose
- name: Verify the dynamic library is actually linked
env:
LIBNGHTTP2_SYS_USE_PKG_CONFIG: "1"
run: |
# Pick the integration test binary: it actually references nghttp2
# symbols, so the linker keeps the DT_NEEDED entry (the lib's own empty
# unit-test harness would be dropped by --as-needed).
bin=$(cargo test --no-run --message-format=json \
| jq -r 'select(.executable != null and .target.name == "integration") | .executable' \
| head -n1)
echo "Test binary: $bin"
ldd "$bin" | grep -i nghttp2
ldd "$bin" | grep -qi 'libnghttp2\.so' \
&& echo "OK: dynamically linked against system libnghttp2" \
|| (echo "ERROR: not linked against system libnghttp2" && exit 1)
13 changes: 13 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,26 @@ homepage = "https://github.com/littledivy/libnghttp2"
description = "FFI bindings to the HTTP/2 framing layer of nghttp2 C library"
readme = "README.md"
license = "Apache-2.0"
# Declare the native library this crate links, so Cargo can enforce a single
# version in the dependency graph and expose DEP_NGHTTP2_* metadata (root,
# include) to downstream build scripts.
links = "nghttp2"

[lib]
doctest = false

[features]
default = []
# Link against a system libnghttp2 (discovered via pkg-config) instead of
# building the bundled copy. If no suitable system library is found, the build
# falls back to the bundled sources. Equivalent to setting the
# LIBNGHTTP2_SYS_USE_PKG_CONFIG environment variable at build time.
system = []

[dependencies]
libc = '0.2'

[build-dependencies]
cc = "1.0.24"
bindgen = "0.71"
pkg-config = "0.3"
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,29 @@ nghttp2_submit_request(session_ptr, ptr::null(), headers.as_ptr(), headers.len()
```

See [examples/h2c_client.rs](examples/h2c_client.rs) for a simple HTTP/2 client implementation.

## Linking

By default this crate builds and statically links the bundled copy of nghttp2,
so it works out of the box with no system dependencies.

Packagers (e.g. distros) can instead link against a system libnghttp2 discovered
via `pkg-config`. This is opt-in and falls back to the bundled build if no
suitable system library is found:

- Enable the `system` Cargo feature:

```toml
[dependencies]
libnghttp2 = { version = "1", features = ["system"] }
```

- Or set the `LIBNGHTTP2_SYS_USE_PKG_CONFIG=1` environment variable at build
time (no source changes required):

```sh
LIBNGHTTP2_SYS_USE_PKG_CONFIG=1 cargo build
```

The crate declares `links = "nghttp2"`, so downstream `-sys`-style consumers can
read `DEP_NGHTTP2_ROOT` and `DEP_NGHTTP2_INCLUDE` from their build scripts.
137 changes: 121 additions & 16 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,20 @@ fn main() {
let out_dir = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR not set"));
let target = env::var("TARGET").expect("TARGET not set");

// Rebuild when the system-linking opt-in changes.
println!("cargo:rerun-if-env-changed=LIBNGHTTP2_SYS_USE_PKG_CONFIG");

// Optionally link against a system libnghttp2 instead of the bundled copy.
// Falls back to the bundled build when no suitable system library is found.
if try_system_nghttp2(&out_dir) {
return;
}

build_bundled(&out_dir, &target);
}

// Build and link the bundled nghttp2 sources (the default / fallback path).
fn build_bundled(out_dir: &Path, target: &str) {
let nghttp2_version = parse_nghttp2_version();

let install_dir = out_dir.join("i");
Expand All @@ -74,17 +88,77 @@ fn main() {
generate_version_header(&include_dir, &nghttp2_version);
copy_main_header(&include_dir);

build_nghttp2(&target, &include_dir, &lib_dir);
build_nghttp2(target, &include_dir, &lib_dir);

generate_pkgconfig(&install_dir, &include_dir, &lib_dir, &nghttp2_version);
generate_bindings(&out_dir, &include_dir);
generate_bindings(out_dir, &include_dir);

println!("cargo:root={}", install_dir.display());
// Expose the bundled headers to downstream build scripts via
// DEP_NGHTTP2_INCLUDE (enabled by `links = "nghttp2"`).
println!("cargo:include={}", include_dir.display());

// Emit rerun-if-changed directives to avoid unnecessary rebuilds
emit_rerun_if_changed();
}

// Returns true if the requested system libnghttp2 was found and linked.
//
// System linking is opt-in: it is attempted only when the `system` Cargo
// feature is enabled or the LIBNGHTTP2_SYS_USE_PKG_CONFIG environment variable
// is set to a truthy value. When opted in but no suitable library is found, we
// return false so the caller falls back to the bundled build.
fn try_system_nghttp2(out_dir: &Path) -> bool {
if !system_linking_requested() {
return false;
}

let mut cfg = pkg_config::Config::new();
// libnghttp2 in this crate tracks the 1.x series; require at least 1.0.0.
cfg.atleast_version("1.0.0");
cfg.print_system_libs(false);

match cfg.probe("libnghttp2") {
Ok(lib) => {
// pkg-config has already emitted the rustc-link-lib / link-search
// directives. Generate bindings against the system headers and expose
// the include paths to downstream crates.
generate_bindings_system(out_dir, &lib.include_paths);

for path in &lib.include_paths {
if let Some(path) = path.to_str() {
println!("cargo:include={}", path);
}
}
println!(
"cargo:warning=libnghttp2: linking against system library (pkg-config)"
);
true
}
Err(err) => {
println!(
"cargo:warning=libnghttp2: system library requested but not found \
({err}); falling back to the bundled build"
);
false
}
}
}

fn system_linking_requested() -> bool {
// `cargo:rustc-cfg`/features reach build scripts as CARGO_FEATURE_* vars.
if env::var_os("CARGO_FEATURE_SYSTEM").is_some() {
return true;
}
match env::var("LIBNGHTTP2_SYS_USE_PKG_CONFIG") {
Ok(value) => {
let value = value.trim();
!value.is_empty() && value != "0" && !value.eq_ignore_ascii_case("false")
}
Err(_) => false,
}
}

fn emit_rerun_if_changed() {
// Build script itself
println!("cargo:rerun-if-changed=build.rs");
Expand Down Expand Up @@ -236,25 +310,56 @@ fn generate_pkgconfig(

fn generate_bindings(out_dir: &Path, include_dir: &Path) {
let header_path = include_dir.join("nghttp2/nghttp2.h");

let mut clang_args = vec![
format!("-I{}", include_dir.display()),
"-Inghttp2/lib/includes".to_string(),
];
clang_args.extend(msvc_ssize_t_clang_arg());

write_bindings(out_dir, header_path.to_str().unwrap(), &clang_args);
}

// Generate bindings against a system-provided libnghttp2 header. A small
// wrapper header is used so the system include directories resolve
// <nghttp2/nghttp2.h>.
fn generate_bindings_system(out_dir: &Path, include_paths: &[PathBuf]) {
let wrapper = out_dir.join("nghttp2_wrapper.h");
fs::write(&wrapper, "#include <nghttp2/nghttp2.h>\n")
.expect("Failed to write bindgen wrapper header");

let mut clang_args: Vec<String> = include_paths
.iter()
.map(|path| format!("-I{}", path.display()))
.collect();
clang_args.extend(msvc_ssize_t_clang_arg());

write_bindings(out_dir, wrapper.to_str().unwrap(), &clang_args);
}

// On Windows MSVC, clang/bindgen needs ssize_t defined.
fn msvc_ssize_t_clang_arg() -> Option<String> {
let target = env::var("TARGET").expect("TARGET not set");
if !(target.contains("windows") && target.contains("msvc")) {
return None;
}

let mut builder = bindgen::Builder::default()
.header(header_path.to_str().unwrap())
.clang_arg(format!("-I{}", include_dir.display()))
.clang_arg("-Inghttp2/lib/includes");
let pointer_width = env::var("CARGO_CFG_TARGET_POINTER_WIDTH")
.expect("CARGO_CFG_TARGET_POINTER_WIDTH not set");

// On Windows MSVC, define ssize_t for clang/bindgen
if target.contains("windows") && target.contains("msvc") {
let pointer_width = env::var("CARGO_CFG_TARGET_POINTER_WIDTH")
.expect("CARGO_CFG_TARGET_POINTER_WIDTH not set");
let ssize_t_def = match pointer_width.as_str() {
"64" => "ssize_t=long long",
"32" => "ssize_t=long",
width => panic!("Unsupported pointer width: {}", width),
};

let ssize_t_def = match pointer_width.as_str() {
"64" => "ssize_t=long long",
"32" => "ssize_t=long",
width => panic!("Unsupported pointer width: {}", width),
};
Some(format!("-D{}", ssize_t_def))
}

builder = builder.clang_arg(format!("-D{}", ssize_t_def));
fn write_bindings(out_dir: &Path, header: &str, clang_args: &[String]) {
let mut builder = bindgen::Builder::default().header(header);
for arg in clang_args {
builder = builder.clang_arg(arg);
}

// Note: We don't use CargoCallbacks here because it would emit
Expand Down
Loading