Skip to content

Commit 167015c

Browse files
authored
Generate Rust Bindings (#2999)
### Issues: * Addresses: V2090958423 ### Description of changes: This adds CMake support for generating Rust FFI bindings from the AWS-LC public C headers using `bindgen-cli`. This is intended for use by downstream Rust consumers so that bindings can be generated directly from the CMake build system. **New CMake option:** `-DGENERATE_RUST_BINDINGS=ON` (off by default) When enabled, the build: 1. Validates that `bindgen-cli` (≥0.69.5) and `rustfmt` are available 2. Auto-discovers all public OpenSSL headers and generates a `rust_wrapper.h` aggregation header 3. Invokes `bindgen` to produce `aws_lc_bindings.rs` in the build directory 4. Supports the existing `BORINGSSL_PREFIX` mechanism — uses `--prefix-link-name` so Rust-side function names stay unprefixed while `#[link_name]` attributes carry the prefix for the linker 5. During install, the bindings are placed at `$INSTALL_DIR/share/rust/aws_lc_bindings.rs` **Files added:** - `cmake/rust_bindings.cmake` — Module containing the header discovery, prefix formatting, and bindgen invocation logic - `cmake/rust_wrapper.h.in` — Template header that aggregates all public headers for bindgen input ### Call-outs: - The prefix symbols header (`boringssl_prefix_symbols.h`) is intentionally excluded from bindgen's input. The rationale is documented in the module-level comment in `cmake/rust_bindings.cmake`. - The `rust_bindings` target is not part of `ALL`. It is built on-demand when explicitly requested (`--target rust_bindings`) or automatically during `cmake --install`. - Platform-specific symbol prefix formatting (Apple/Win32 leading underscore) is handled by `get_symbol_prefix_format()`. ### Testing: CI workflow added in `.github/workflows/aws-lc-rs.yml` (`cmake-rust-bindings` job) covering: - Linux (no prefix) - Linux with `BORINGSSL_PREFIX=AWSLC` - macOS (no prefix) - macOS with `BORINGSSL_PREFIX=AWSLC` - Windows (no prefix) Each matrix entry verifies that bindings are generated, contain expected symbols, have correct prefix behavior, and compile as valid Rust. By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license and the ISC license.
1 parent 6eff17e commit 167015c

File tree

4 files changed

+524
-0
lines changed

4 files changed

+524
-0
lines changed

.github/workflows/aws-lc-rs.yml

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,3 +221,181 @@ jobs:
221221
cargo clean
222222
cargo test -p aws-lc-sys --target x86_64-pc-windows-msvc
223223
cargo test -p aws-lc-rs --target x86_64-pc-windows-msvc
224+
225+
# CMake Rust bindings generation tests
226+
cmake-rust-bindings:
227+
if: github.repository_owner == 'aws'
228+
strategy:
229+
fail-fast: false
230+
matrix:
231+
include:
232+
- os: ubuntu-latest
233+
name: linux
234+
- os: ubuntu-latest
235+
name: linux-prefix
236+
prefix: AWSLC_PREFIX
237+
- os: macos-latest
238+
name: macos
239+
- os: macos-latest
240+
name: macos-prefix
241+
prefix: AWSLC_PREFIX
242+
- os: windows-latest
243+
name: windows
244+
- os: ubuntu-latest
245+
name: linux-no-ssl
246+
build_libssl: OFF
247+
runs-on: ${{ matrix.os }}
248+
name: cmake-rust-bindings (${{ matrix.name }})
249+
steps:
250+
- uses: actions/checkout@v4
251+
- uses: dtolnay/rust-toolchain@stable
252+
with:
253+
components: 'rustfmt'
254+
- uses: ilammy/setup-nasm@v1
255+
if: runner.os == 'Windows'
256+
- uses: actions/setup-go@v4
257+
with:
258+
go-version: '>=1.20'
259+
- name: Install bindgen-cli
260+
run: cargo install --force --locked bindgen-cli
261+
262+
# Prefix builds need a non-prefixed build first to collect symbols
263+
- name: Generate prefix symbols file
264+
if: matrix.prefix
265+
shell: bash
266+
run: |
267+
cmake -B build-noprefix -DBUILD_TESTING=OFF
268+
cmake --build build-noprefix
269+
go run ./util/read_symbols.go build-noprefix/crypto/libcrypto.a > symbols.txt
270+
go run ./util/read_symbols.go build-noprefix/ssl/libssl.a >> symbols.txt
271+
echo "Collected $(wc -l < symbols.txt) symbols"
272+
273+
- name: Configure with Rust bindings generation
274+
shell: bash
275+
run: |
276+
cmake_args="-DGENERATE_RUST_BINDINGS=ON -DBUILD_TESTING=OFF"
277+
if [ -n "${{ matrix.prefix }}" ]; then
278+
cmake_args="$cmake_args -DBORINGSSL_PREFIX=${{ matrix.prefix }}"
279+
cmake_args="$cmake_args -DBORINGSSL_PREFIX_SYMBOLS=$(pwd)/symbols.txt"
280+
fi
281+
if [ "${{ matrix.build_libssl }}" = "OFF" ]; then
282+
cmake_args="$cmake_args -DBUILD_LIBSSL=OFF"
283+
fi
284+
cmake -B build $cmake_args
285+
286+
- name: Build libraries
287+
run: cmake --build build --config Release
288+
289+
- name: Generate bindings
290+
run: cmake --build build --target rust_bindings --config Release --verbose
291+
292+
- name: Verify bindings file exists
293+
shell: bash
294+
run: |
295+
if [ ! -f build/rust/aws_lc_bindings.rs ]; then
296+
echo "ERROR: Rust bindings file was not generated"
297+
exit 1
298+
fi
299+
echo "Generated bindings file size: $(wc -c < build/rust/aws_lc_bindings.rs) bytes"
300+
echo "Generated bindings line count: $(wc -l < build/rust/aws_lc_bindings.rs) lines"
301+
302+
- name: Verify bindings content
303+
shell: bash
304+
run: |
305+
# Verify SSL bindings based on BUILD_LIBSSL setting (defaults to ON)
306+
if [ "${{ matrix.build_libssl }}" != "OFF" ]; then
307+
if ! grep -q "pub fn SSL_new" build/rust/aws_lc_bindings.rs; then
308+
echo "ERROR: Expected SSL_new function not found"
309+
exit 1
310+
fi
311+
else
312+
if grep -q "pub fn SSL_new" build/rust/aws_lc_bindings.rs; then
313+
echo "ERROR: Unexpected SSL_new found in BUILD_LIBSSL=OFF build"
314+
exit 1
315+
fi
316+
echo "Confirmed: SSL bindings correctly excluded"
317+
fi
318+
if [ -n "${{ matrix.prefix }}" ]; then
319+
# Prefix builds: link_name attributes should contain the prefix.
320+
# The exact format varies by platform (e.g., _PREFIX_ on macOS vs PREFIX_ on Linux).
321+
if ! grep -q 'link_name.*${{ matrix.prefix }}_' build/rust/aws_lc_bindings.rs; then
322+
echo "ERROR: Expected prefixed link_name attributes not found"
323+
exit 1
324+
fi
325+
if ! grep -B1 "pub fn SSL_new" build/rust/aws_lc_bindings.rs | grep -q 'link_name.*${{ matrix.prefix }}_'; then
326+
echo "ERROR: SSL_new should have ${{ matrix.prefix }}_ prefixed link_name"
327+
exit 1
328+
fi
329+
else
330+
# Non-prefix builds should not have link_name attributes
331+
if grep -q '#\[link_name' build/rust/aws_lc_bindings.rs; then
332+
echo "ERROR: Unexpected link_name attributes found in no-prefix build"
333+
exit 1
334+
fi
335+
fi
336+
echo "Bindings content verification passed"
337+
338+
339+
- name: Verify bindings build and link
340+
shell: bash
341+
run: |
342+
mkdir -p "${RUNNER_TEMP}/test-bindings/src"
343+
cat > "${RUNNER_TEMP}/test-bindings/Cargo.toml" << 'EOF'
344+
[package]
345+
name = "test-bindings"
346+
version = "0.1.0"
347+
edition = "2021"
348+
349+
[[bin]]
350+
name = "test-bindings"
351+
path = "src/main.rs"
352+
EOF
353+
cat > "${RUNNER_TEMP}/test-bindings/build.rs" << 'EOF'
354+
use std::env;
355+
fn main() {
356+
let build_dir = env::var("CMAKE_BUILD_DIR").expect("CMAKE_BUILD_DIR must be set");
357+
// Library search paths for single-config generators (Unix Makefiles, Ninja)
358+
println!("cargo:rustc-link-search=native={}/crypto", build_dir);
359+
println!("cargo:rustc-link-search=native={}/ssl", build_dir);
360+
// Library search paths for multi-config generators (Visual Studio)
361+
println!("cargo:rustc-link-search=native={}/crypto/Release", build_dir);
362+
println!("cargo:rustc-link-search=native={}/ssl/Release", build_dir);
363+
println!("cargo:rustc-link-lib=static=crypto");
364+
if env::var("INCLUDE_SSL").unwrap_or_default() == "1" {
365+
println!("cargo:rustc-link-lib=static=ssl");
366+
}
367+
// Platform-specific system library dependencies required by aws-lc
368+
let target_family = env::var("CARGO_CFG_TARGET_FAMILY").unwrap_or_default();
369+
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
370+
if target_family == "unix" {
371+
println!("cargo:rustc-link-lib=dylib=pthread");
372+
}
373+
if target_os == "windows" {
374+
println!("cargo:rustc-link-lib=dylib=ws2_32");
375+
}
376+
}
377+
EOF
378+
cat > "${RUNNER_TEMP}/test-bindings/src/main.rs" << 'EOF'
379+
#![allow(clippy::all)]
380+
#![allow(non_upper_case_globals)]
381+
#![allow(non_camel_case_types)]
382+
#![allow(non_snake_case)]
383+
#![allow(dead_code)]
384+
#![allow(improper_ctypes)]
385+
#![allow(unpredictable_function_pointer_comparisons)]
386+
include!(concat!(env!("BINDINGS_PATH"), "/aws_lc_bindings.rs"));
387+
fn main() {
388+
unsafe { CRYPTO_library_init(); }
389+
println!("Bindings link test passed");
390+
}
391+
EOF
392+
cd "${RUNNER_TEMP}/test-bindings"
393+
include_ssl="0"
394+
if [ "${{ matrix.build_libssl }}" != "OFF" ]; then
395+
include_ssl="1"
396+
fi
397+
export CMAKE_BUILD_DIR="${GITHUB_WORKSPACE}/build"
398+
export BINDINGS_PATH="${GITHUB_WORKSPACE}/build/rust"
399+
export INCLUDE_SSL="${include_ssl}"
400+
cargo run
401+
echo "Bindings build, link, and run test passed"

CMakeLists.txt

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ option(ENABLE_DATA_INDEPENDENT_TIMING "Enable automatic setting/resetting Data-I
9696
option(ENABLE_PRE_SONAME_BUILD "Build AWS-LC without SONAME configuration for shared library builds" ON)
9797
option(ENABLE_SOURCE_MODIFICATION "Allow the build to update files in the source directory. This is typically done to update versioning." ON)
9898
option(DISABLE_CPU_JITTER_ENTROPY "Disable usage of CPU Jitter Entropy as an entropy source. This option cannot be used with the FIPS build. With this configuration, randomness generation might not use two independent entropy sources." OFF)
99+
option(GENERATE_RUST_BINDINGS "Generate Rust bindings using bindgen-cli" OFF)
100+
set(RUST_BINDINGS_TARGET_VERSION "1.70" CACHE STRING "Minimum Rust version for generated bindings")
99101

100102
include(cmake/go.cmake)
101103

@@ -112,6 +114,47 @@ message(STATUS "PERFORM_SONAME_BUILD: ${PERFORM_SONAME_BUILD}")
112114

113115
enable_language(C)
114116

117+
# Validate Rust bindings prerequisites
118+
if(GENERATE_RUST_BINDINGS)
119+
find_program(BINDGEN_EXECUTABLE NAMES bindgen)
120+
if(NOT BINDGEN_EXECUTABLE)
121+
message(FATAL_ERROR "GENERATE_RUST_BINDINGS is enabled but bindgen-cli was not found. "
122+
"Install it with: cargo install --force --locked bindgen-cli")
123+
endif()
124+
125+
# Verify minimum version (0.69.5)
126+
execute_process(
127+
COMMAND ${BINDGEN_EXECUTABLE} --version
128+
OUTPUT_VARIABLE BINDGEN_VERSION_OUTPUT
129+
OUTPUT_STRIP_TRAILING_WHITESPACE
130+
RESULT_VARIABLE BINDGEN_VERSION_RESULT
131+
)
132+
133+
if(NOT BINDGEN_VERSION_RESULT EQUAL 0)
134+
message(FATAL_ERROR "Failed to get bindgen version")
135+
endif()
136+
137+
# Extract version number (format: "bindgen X.Y.Z")
138+
string(REGEX MATCH "[0-9]+\\.[0-9]+\\.[0-9]+" BINDGEN_VERSION "${BINDGEN_VERSION_OUTPUT}")
139+
140+
if(BINDGEN_VERSION VERSION_LESS "0.69.5")
141+
message(FATAL_ERROR "bindgen version ${BINDGEN_VERSION} is too old. "
142+
"Minimum required version is 0.69.5. "
143+
"Upgrade with: cargo install --force --locked bindgen-cli")
144+
endif()
145+
146+
message(STATUS "Found bindgen-cli: ${BINDGEN_EXECUTABLE} (version ${BINDGEN_VERSION})")
147+
message(STATUS "Rust bindings target version: ${RUST_BINDINGS_TARGET_VERSION}")
148+
149+
# Check for rustfmt (required for --formatter rustfmt option)
150+
find_program(RUSTFMT_EXECUTABLE NAMES rustfmt)
151+
if(NOT RUSTFMT_EXECUTABLE)
152+
message(FATAL_ERROR "GENERATE_RUST_BINDINGS requires rustfmt but it was not found. "
153+
"Install it with: rustup component add rustfmt")
154+
endif()
155+
message(STATUS "Found rustfmt: ${RUSTFMT_EXECUTABLE}")
156+
endif()
157+
115158
if(DISABLE_CPU_JITTER_ENTROPY)
116159
if(FIPS)
117160
message(FATAL_ERROR "Cannot opt-out of CPU Jitter for the FIPS build")
@@ -364,6 +407,60 @@ else()
364407
)
365408
endif()
366409

410+
# Rust bindings generation
411+
if(GENERATE_RUST_BINDINGS)
412+
include(cmake/rust_bindings.cmake)
413+
414+
# Generate the rust_wrapper.h header from template
415+
generate_rust_wrapper_header()
416+
417+
set(RUST_BINDINGS_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/rust/aws_lc_bindings.rs")
418+
419+
# Build list of include directories for bindgen.
420+
# Prefix symbols header is intentionally excluded; see cmake/rust_bindings.cmake.
421+
set(RUST_BINDINGS_INCLUDE_DIRS
422+
"${CMAKE_CURRENT_BINARY_DIR}/include" # Generated rust_wrapper.h location
423+
"${AWSLC_SOURCE_DIR}/include" # Main AWS-LC headers (unprefixed)
424+
)
425+
426+
# Conditionally set optional arguments for generate_rust_bindings
427+
if(BORINGSSL_PREFIX)
428+
set(_rust_prefix_arg PREFIX "${BORINGSSL_PREFIX}")
429+
else()
430+
set(_rust_prefix_arg "")
431+
endif()
432+
433+
if(BUILD_LIBSSL)
434+
set(_rust_ssl_arg INCLUDE_SSL)
435+
else()
436+
set(_rust_ssl_arg "")
437+
endif()
438+
439+
generate_rust_bindings(
440+
OUTPUT_FILE "${RUST_BINDINGS_OUTPUT}"
441+
INCLUDE_DIRS ${RUST_BINDINGS_INCLUDE_DIRS}
442+
${_rust_prefix_arg}
443+
${_rust_ssl_arg}
444+
)
445+
446+
# Install the generated Rust bindings.
447+
# The rust_bindings target is not part of ALL, so we build it on-demand during install.
448+
install(CODE "execute_process(COMMAND \"${CMAKE_COMMAND}\" --build \"${CMAKE_BINARY_DIR}\" --target rust_bindings)")
449+
450+
install(
451+
FILES "${CMAKE_CURRENT_BINARY_DIR}/rust/aws_lc_bindings.rs"
452+
DESTINATION "share/rust"
453+
COMPONENT Development
454+
)
455+
456+
# Install the generated rust_wrapper.h header
457+
install(
458+
FILES "${CMAKE_CURRENT_BINARY_DIR}/include/rust_wrapper.h"
459+
DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}"
460+
COMPONENT Development
461+
)
462+
endif()
463+
367464

368465
if("${CMAKE_SYSTEM_NAME}" STREQUAL "Emscripten")
369466
set(EMSCRIPTEN 1)

0 commit comments

Comments
 (0)