Module kusabira::builder

source ·
Expand description

The build frontend.

builder::Config holds the builder configuration. It is created with the default parameters by builder::Config::default.

The builder is then configured by the methods on builder::Config. Both the source files (*.c, *.cc, *.s, …) and header files (*.h, *.hh, …) can be configured together as the input files. At least one input file MUST be configured, either source or header.

The backends can also be configured via builder::Config. In order to make the kusabira API robust against the changes on the backend configuration design, they are configured by the hooks, namely the user-supplied function or closure that receives the backend-dependent configuration, modifies it and returns it.

Finally, the builder is executed by builder::Config::build, which builds the library and Rust FFI binding files altogether. builder::Config::build checks the extention of each input file; the source files are passed together to cc::Build::try_compile to build a single library, while the header files are passed in the one-by-one manner to bindgen::Builder::generate and bindgen::Bindings::write_to_file. Both of these behaviors reflect the usage design of the backends.

The glob expansion by glob is supported on the input files. The glob expansion happens during the execution of builder::Config::build.

All of the input files discovered by builder::Config::build are reported to Cargo by cargo:rerun-if-changed, so that an update on any input files trigger the rebuild. This includes the recursively included header files found by bindgen::Builder::generate.

Source files

Configure all of them together, possibly by the glob. They are all compiled into a single library, which is then reported to Cargo by builder::Config::build for linking.

Header files

Create a single header file that #includes all of the header files exported to Rust. Configure only this header file to builder::Config. On the Rust side, include! the generated binding file into a Rust source file. Each binding file has the same filename as the configured header file except that the extention is replaced by the one configured by builder::Config::binding_ext, or builder::RUST_FFI_BINDING_EXT by default.

This configuration is recommended because bindgen::Builder::generate requires a header file completely pre-processable and compilable on its own, while most header files depend on some other header files #included before them.

Mind that the identifier scope of Rust is different from C/C++/assembly. In Rust, you include! a binding file somewhere in a module, and all of the identifiers in it are visible anywhere in the module.

Also note that the generated binding file SHOULD NOT be compiled directly. rustc assumes the certain source file hierarchy to define the modules, which requires some tricks to follow. In addition, Cargo may exhibit an error or unexpected behavior if the build script emits any files into the source directory.

A good practice in a large-scaled project is to include! the binding file into a dedicated module, and use the required items only to avoid flooding a module by many unused identifiers. As of version 0.68.1, bindgen adds pub to every bound identifier.

Alternatively, you can also create multiple header files and configure all of them, as long as each header file pre-processes and compiles on its own. This is a good option if you have multiple features to import and each Rust module does not require all imported features.

Internal Design Notes

Path Storage

The output directory is stored as std::path::PathBuf because it MUST point to a valid directory path upon building.

In contrary, the source file glob patterns are represented by std::string::String. A glob pattern do not necessarily have to be a valid path, so std::path::PathBuf is too restrictive. glob deals with a pattern in the same way.

Hook Addition

The following methods on builder::Config add a new hook to the existing one in the configuration:

Internally, these methods create a new hook closure in which the input parameter is first passed to the existing hook, and its result is then passed to the new hook. Below is the pseudocode of the internal hook closure created by builder::Config::add_cc_build_hook:

use cc::Build;
use std::ops::FnOnce;

let existing_hook: Box<dyn FnOnce(&mut Build) -> &mut Build>
	= Box::new(|build: &mut Build|
		{
			// Work on build.
			build
		});
let added_hook: Box<dyn FnOnce(&mut Build) -> &mut Build>
	= Box::new(|build: &mut Build|
		{
			// Another work on build.
			build
		});

let new_hook: Box<dyn FnOnce(&mut Build) -> &mut Build>
	= Box::new(
		move |build: &mut Build|
		{
			added_hook(existing_hook(build))
		});

The other methods create the internal hook closure in the same way except for the parameter and return type, and the hook function trait.

Structs

Statics