Skip to content

Commit

Permalink
Build Rust executables
Browse files Browse the repository at this point in the history
Rename 'app' to 'app_c', and add 'app_rust' to demo the new
`add_rust_executable()` CMake function.

In 'app_rust', I also demo using a relative path to bring in Rust crates
from the same project.

I added a 'build.rs' file for both running unit tests for the Rust
library, to automatically build with cbindgen and bindgen, and also a
'build.rs' for linking 'app_rust' with any C library dependencies, and
running 'bindgen' for that as well.

In the 'gen_uuid' module, I now export both a native rust API as well as
an `extern "C"` API so it can be used from both C and from Rust.
  • Loading branch information
val-ms committed Mar 31, 2022
1 parent 1a5c950 commit 24baafd
Show file tree
Hide file tree
Showing 16 changed files with 383 additions and 58 deletions.
30 changes: 24 additions & 6 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,26 @@ jobs:
- name: Build project
working-directory: "${{ runner.workspace }}/"
run: |
cmake -B ./build -S ./cmake-rust-demo
cmake -B ./build -S ./cmake-rust-demo -D CMAKE_INSTALL_PREFIX=${{ runner.workspace }}/install
cmake --build ./build
- name: Run tests
working-directory: "${{ runner.workspace }}/build/"
run: |
ctest -VV
- name: Run the app
- name: Install project
working-directory: "${{ runner.workspace }}/"
run: |
cmake --build ./build --target install
- name: Run the C app
run: |
${{ runner.workspace }}/install/bin/app_c
- name: Run the Rust app
run: |
${{ runner.workspace }}/build/app/app
${{ runner.workspace }}/install/bin/app_rust
build_and_test_windows:
name: "windows-latest: build and test"
Expand All @@ -62,14 +71,23 @@ jobs:
- name: Build project
working-directory: "${{ runner.workspace }}/"
run: |
cmake -B ./build -S ./cmake-rust-demo -A x64
cmake -B ./build -S ./cmake-rust-demo -A x64 -D CMAKE_INSTALL_PREFIX=${{ runner.workspace }}/install
cmake --build ./build
- name: Run tests
working-directory: "${{ runner.workspace }}/build/"
run: |
ctest -VV -C Debug
- name: Run the app
- name: Install project
working-directory: "${{ runner.workspace }}/"
run: |
cmake --build ./build --target install
- name: Run the C app
run: |
${{ runner.workspace }}\\install\\bin\\app_c.exe
- name: Run the Rust app
run: |
${{ runner.workspace }}\\build\\app\\Debug\\app.exe
${{ runner.workspace }}\\install\\bin\\app_rust.exe
3 changes: 2 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ include(CPack)
#
add_subdirectory(lib)
add_subdirectory(common)
add_subdirectory(app)
add_subdirectory(app_c)
add_subdirectory(app_rust)
add_subdirectory(test)

#
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]

members = ["lib/rust", "common/gen_uuid"]
members = ["lib/rust", "common/gen_uuid", "app_rust"]

[profile.dev.package."*"]
opt-level = 2
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,20 @@ or:
install(TARGETS app DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT programs)
```
Rust library CMake targets aren't normal CMake binary targets though. They're "custom" targets, which means you will instead have to use `install(FILES` instead of `install(TARGETS`, and then point CMake at the specific file you need installed instead of at a target. Our `FindRust.cmake`'s `add_rust_library()` function makes this easy. WHen you add a Rust library, it sets the target properties such that you can simply use CMake's `$<TARGET_FILE:target>` [generator expression](https://cmake.org/cmake/help/latest/manual/cmake-generator-expressions.7.html) to provide the file path. In this demo, we configure installation for our `demorust` Rust static library like this:
Rust library CMake targets aren't normal CMake binary targets though. They're "custom" targets, which means you will instead have to use `install(FILES` instead of `install(TARGETS`, and then point CMake at the specific file you need installed instead of at a target. Our `FindRust.cmake`'s `add_rust_library()` function makes this easy. WHen you add a Rust library, it sets the target properties such that you can simply use CMake's `$<TARGET_FILE:target>` [generator expression](https://cmake.org/cmake/help/latest/manual/cmake-generator-expressions.7.html) to provide the file path.
In this demo, we configure installation for our `demorust` Rust static library like this:
```c
install(FILES $<TARGET_FILE:demorust> DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT libraries)
```

And for our `app_rust` Rust executable, we install like this:
```c
get_target_property(app_rust_EXECUTABLE app_rust IMPORTED_LOCATION)
install(PROGRAMS ${app_rust_EXECUTABLE} DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT programs)
```
Note that we have to get the `IMPORTED_LOCATION` manually for the executable.
## License
This project is dual-licensed under MIT and Apache 2.0.
Expand Down
21 changes: 0 additions & 21 deletions app/CMakeLists.txt

This file was deleted.

22 changes: 22 additions & 0 deletions app_c/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Copyright (C) 2022 Micah Snyder.

#
# C executable
#
# Example app that used a static lib written in Rust and a traditional
# shared library written in C with some Rust sprinkled in.
#

add_executable(app_c)
target_sources(app_c PRIVATE app.c)
target_link_libraries(app_c
PRIVATE
demo::rust_static_lib
demo::static_lib
demo::gen_uuid)
if(WIN32)
target_link_libraries(app_c
PRIVATE
Bcrypt)
endif()
install(TARGETS app_c DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT programs)
File renamed without changes.
13 changes: 13 additions & 0 deletions app_rust/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copyright (C) 2022 Micah Snyder.

#
# Rust executable
#
# Example app that links with our C (and Rust) libs.
#

add_rust_executable(TARGET app_rust WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/.")
# Linking with C libraries is done within `build.rs`
# Linking with nearby Rust crates is done within `Cargo.toml`
get_target_property(app_rust_EXECUTABLE app_rust IMPORTED_LOCATION)
install(PROGRAMS ${app_rust_EXECUTABLE} DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT programs)
11 changes: 11 additions & 0 deletions app_rust/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
authors = ["Micah Snyder"]
edition = "2021"
name = "app_rust"
version = "0.1.0"

[dependencies]
gen_uuid = { path = "../common/gen_uuid" }

[build-dependencies]
bindgen = "0.59"
193 changes: 193 additions & 0 deletions app_rust/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
use std::env;
use std::path::{Path, PathBuf};

use bindgen::builder;

// A list of environment variables to query to determine additional libraries
// that need to be linked to resolve dependencies.
const LIB_ENV_LINK: &[&str] = &["LIBDEMO"];

// Additional [verbatim] libraries to link on Windows platforms
const LIB_LINK_WINDOWS: &[&str] = &["wsock32", "ws2_32", "Shell32", "User32"];

// Generate bindings for these functions:
const BINDGEN_FUNCTIONS: &[&str] = &["do_the_thing"];

// Generate bindings for these types (structs, enums):
const BINDGEN_TYPES: &[&str] = &[];

// Find the required functions and types in these headers:
const BINDGEN_HEADERS: &[&str] = &["../c/dostuff.h"];

// Find the required headers in these directories:
const BINDGEN_INCLUDE_PATHS: &[&str] = &["-I../lib/c"];

// Write the bindings to this file:
const BINDGEN_OUTPUT_FILE: &str = "src/sys.rs";

// Environment variable name prefixes worth including for diags
const ENV_PATTERNS: &[&str] = &["CARGO_", "RUST", "LIB"];

fn main() -> Result<(), &'static str> {
eprintln!("build.rs command line: {:?}", std::env::args());
eprintln!("Environment:");
std::env::vars()
.filter(|(k, _)| ENV_PATTERNS.iter().any(|prefix| k.starts_with(prefix)))
.for_each(|(k, v)| eprintln!(" {}={:?}", k, v));

// We only want to generate bindings for `cargo build`, not `cargo test`.
// FindRust.cmake defines $CARGO_CMD so we can differentiate.
let cargo_cmd = env::var("CARGO_CMD").unwrap_or_else(|_| "".into());

match cargo_cmd.as_str() {
"build" => {
println!("cargo:rerun-if-env-changed=LIBDEMO");

// Generate bindings as a part of the build.

let maintainer_mode = env::var("MAINTAINER_MODE").unwrap_or_else(|_| "".into());
if maintainer_mode == "ON" {
// Only generate the `.rs` bindings when maintainer-mode is enabled.
// Bindgen requires libclang, which may not readily available, so we will commit the
// bindings to version control and use maintainer-mode to update them, as needed.
// On the plus-side, this means that our `.rs` file is present before our first build,
// so at least rust-analyzer will be happy.
generate_rust_bindings()?;
}

// Link executable with library dependencies.
for var in LIB_ENV_LINK {
if !search_and_link_lib(var)? {
eprintln!("Undefined library dependency environment variable: {}", var);
return Err("Undefined library dependency environment variable");
}
}

if cfg!(windows) {
for lib in LIB_LINK_WINDOWS {
println!("cargo:rustc-link-lib={}", lib);
}
}
}

_ => {
return Ok(());
}
}

Ok(())
}

/// Use bindgen to generate Rust bindings to call into C libraries.
fn generate_rust_bindings() -> Result<(), &'static str> {
let build_dir = PathBuf::from(env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| ".".into()));
let build_include_path = format!("-I{}", build_dir.join("..").to_str().unwrap());

// Configure and generate bindings.
let mut builder = builder()
// Silence code-style warnings for generated bindings.
.raw_line("#![allow(non_snake_case, non_camel_case_types, non_upper_case_globals)]")
// Make the bindings pretty.
.rustfmt_bindings(true)
// Disable the layout tests because we're committing `sys.rs` to source control.
// Pointer width, integer size, etc. are probably not the same when generated as when compiled.
.layout_tests(false)
// Enable bindgen to find generated headers in the build directory, too.
.clang_arg(build_include_path);

for &include_path in BINDGEN_INCLUDE_PATHS {
builder = builder.clang_arg(include_path);
}
for &header in BINDGEN_HEADERS {
builder = builder.header(header);
}
for &c_function in BINDGEN_FUNCTIONS {
builder = builder.allowlist_function(c_function);
}
for &c_type in BINDGEN_TYPES {
builder = builder.allowlist_type(c_type);
}

// Generate!
builder
.generate()
.expect("Unable to generate Rust bindings for C code")
.write_to_file(BINDGEN_OUTPUT_FILE)
.expect("Failed to write Rust bindings to output file");

Ok(())
}

/// Return whether the specified environment variable has been set, and output
/// linking directives as a side-effect
fn search_and_link_lib(environment_variable: &str) -> Result<bool, &'static str> {
eprintln!(" - checking for {:?} in environment", environment_variable);
let filepath_str = match env::var(environment_variable) {
Err(env::VarError::NotPresent) => return Ok(false),
Err(env::VarError::NotUnicode(_)) => return Err("environment value not unicode"),
Ok(s) => {
if s.is_empty() {
return Ok(false);
} else {
s
}
}
};

let parsed_path = parse_lib_path(&filepath_str)?;
eprintln!(
" - adding {:?} to rustc library search path",
&parsed_path.dir
);
println!("cargo:rustc-link-search={}", parsed_path.dir);
eprintln!(" - requesting that rustc link {:?}", &parsed_path.libname);
println!("cargo:rustc-link-lib={}", parsed_path.libname);

Ok(true)
}

/// Struct to store a lib name and directory.
/// Not the
struct ParsedLibraryPath {
dir: String,
libname: String,
}

/// Parse a library path, returning:
/// - the directory containing the library
/// - the portion expected after the `-l`
fn parse_lib_path<'a>(path: &'a str) -> Result<ParsedLibraryPath, &'static str> {
let path = PathBuf::from(path);
let file_name = path
.file_name()
.ok_or("file name not found")?
.to_str()
.ok_or("file name not unicode")?;

// This can't fail because it came from a &str
let dir = path
.parent()
.unwrap_or_else(|| Path::new("."))
.to_str()
.unwrap()
.to_owned();

// Grab the portion up to the first '.'
let full_libname = file_name
.split('.')
.next()
.ok_or("no '.' found in file name")?;

let libname = if !cfg!(windows) {
// Trim off the "lib" for Linux/Unix systems
full_libname
.strip_prefix("lib")
.ok_or(r#"file name doesn't begin with "lib""#)?
} else {
// Keep the full libname on Windows.
full_libname
}
.to_owned();

Ok(ParsedLibraryPath { dir, libname })
}
10 changes: 10 additions & 0 deletions app_rust/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
* Example rust app.
*
* Copyright (C) 2022 Micah Snyder.
*/

fn main() {
let uuid = gen_uuid::gen_uuid();
println!("Hello, {}!", uuid);
}
Loading

0 comments on commit 24baafd

Please sign in to comment.