├── .gitignore ├── examples ├── local │ ├── src │ │ ├── header.h │ │ └── main.rs │ ├── build.rs │ └── Cargo.toml ├── cpp │ ├── src │ │ └── main.rs │ └── Cargo.toml ├── system │ ├── Cargo.toml │ └── src │ │ └── main.rs └── nostd │ ├── Cargo.toml │ └── src │ └── main.rs ├── Cargo.toml ├── src ├── lib.rs └── utils.rs ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /examples/local/src/header.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include -------------------------------------------------------------------------------- /examples/local/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("cargo:rustc-link-lib=cairo"); 3 | } -------------------------------------------------------------------------------- /examples/cpp/src/main.rs: -------------------------------------------------------------------------------- 1 | c_import::cpp_import!( 2 | "", 3 | "--link fltk" 4 | ); 5 | 6 | fn main() { 7 | println!("{}", unsafe { Fl::abi_version() }); 8 | } 9 | -------------------------------------------------------------------------------- /examples/cpp/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cpp" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | c_import = { path = "../.." } -------------------------------------------------------------------------------- /examples/local/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "local" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | c_import = { path = "../.." } -------------------------------------------------------------------------------- /examples/system/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "system_include" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | c_import = { path = "../.." } -------------------------------------------------------------------------------- /examples/system/src/main.rs: -------------------------------------------------------------------------------- 1 | use c_import::c_import; 2 | 3 | c_import!( 4 | "", 5 | "", 6 | "--link cairo" 7 | ); 8 | 9 | fn main() { 10 | let version = unsafe { cairo_version() }; 11 | println!("{}", version); 12 | } 13 | -------------------------------------------------------------------------------- /examples/local/src/main.rs: -------------------------------------------------------------------------------- 1 | mod cairo { 2 | use c_import::c_import; 3 | c_import!( 4 | "header.h", 5 | "-Isrc" 6 | ); 7 | } 8 | 9 | fn main() { 10 | let version = unsafe { cairo::cairo_version() }; 11 | println!("{}", version); 12 | } 13 | -------------------------------------------------------------------------------- /examples/nostd/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nostd" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | c_import = { path = "../.." } 10 | defer-lite = "*" 11 | 12 | [profile.dev] 13 | panic = "abort" 14 | 15 | [profile.release] 16 | panic = "abort" 17 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "c_import" 3 | version = "0.2.4" 4 | edition = "2021" 5 | description = "A c_import macro for Rust" 6 | repository = "https://github.com/MoAlyousef/c_import" 7 | keywords = ["bindings", "ffi", "code-generation"] 8 | categories = ["external-ffi-bindings", "development-tools::ffi"] 9 | readme = "README.md" 10 | license = "MIT" 11 | 12 | [lib] 13 | proc-macro = true 14 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | #![allow(clippy::needless_doctest_main)] 3 | 4 | use proc_macro::TokenStream; 5 | 6 | mod utils; 7 | 8 | #[proc_macro] 9 | pub fn c_import(input: TokenStream) -> TokenStream { 10 | utils::common(input, false) 11 | } 12 | 13 | #[proc_macro] 14 | pub fn cpp_import(input: TokenStream) -> TokenStream { 15 | utils::common(input, true) 16 | } 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Mohammed Alyousef 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /examples/nostd/src/main.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | 4 | use core::ffi::*; 5 | use defer_lite::defer; 6 | 7 | 8 | c_import::c_import!( 9 | "", 10 | "", 11 | "", 12 | "$pkg-config --cflags cairo", 13 | "--link cairo", 14 | "--link c" 15 | ); 16 | 17 | #[no_mangle] 18 | pub extern "C" fn main(argc: c_int, argv: *const *const c_char) -> c_int { 19 | unsafe { 20 | let args = core::slice::from_raw_parts(argv, argc as _); 21 | for (i, arg) in args.iter().enumerate() { 22 | printf("arg %d is %s\n\0".as_ptr() as _, i, *arg); 23 | } 24 | let version = cairo_version(); 25 | let msg_len = snprintf(core::ptr::null_mut(), 0, "Cairo version is: %d\n\0".as_ptr() as _, version); 26 | let buf: *mut c_char = malloc(msg_len as _) as _; 27 | defer!(free(buf as _)); 28 | snprintf(buf, msg_len as _, "Cairo version is: %d\n\0".as_ptr() as _, version); 29 | printf("%s\n\0".as_ptr() as _, buf); 30 | } 31 | 0 32 | } 33 | 34 | #[no_mangle] 35 | extern "C" fn rust_eh_personality() {} 36 | 37 | #[allow(non_snake_case)] 38 | #[no_mangle] 39 | extern "C" fn _Unwind_Resume() {} 40 | 41 | #[panic_handler] 42 | fn ph(_: &core::panic::PanicInfo) -> ! { 43 | loop {} 44 | } 45 | 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # c_import 2 | 3 | This is a small proc macro crate providing a c_import macro (also a cpp_import macro), which can be used to import C headers into your program. It leverages [bindgen](https://github.com/rust-lang/rust-bindgen), so bindgen needs to be installed in your system. 4 | It also works in no_std mode. 5 | 6 | ## Usage 7 | In your Cargo.toml: 8 | ```toml 9 | # Cargo.toml 10 | 11 | [depenedencies] 12 | c_import = "0.2" 13 | ``` 14 | 15 | In your Rust source file: 16 | ```rust 17 | // src/main.rs 18 | use c_import::c_import; 19 | 20 | c_import!( 21 | "", 22 | "", 23 | "--link cairo" 24 | ); 25 | 26 | fn main() { 27 | let version = unsafe { cairo_version() }; 28 | println!("{}", version); 29 | } 30 | ``` 31 | 32 | If you don't use the `--link` directive, you can use a Rust build script: 33 | ```rust 34 | // build.rs 35 | fn main() { 36 | println!("cargo:rustc-link-lib=cairo"); 37 | } 38 | ``` 39 | 40 | Using non-system headers is also possible via enclosing the header path with quotation marks: 41 | ```rust 42 | // src/main.rs 43 | use c_import::c_import; 44 | c_import!("src/my_header.h"); 45 | 46 | fn main() { 47 | let version = unsafe { cairo_version() }; 48 | println!("{}", version); 49 | } 50 | ``` 51 | 52 | ## Extra clang arguments 53 | You can pass extra clang arguments as extra arguments to the macro: 54 | ```rust 55 | // src/main.rs 56 | use c_import::c_import; 57 | c_import!( 58 | "src/my_header.h", 59 | "-DMY_DEFINE", 60 | "-I/somepath/include" 61 | ); 62 | 63 | fn main() { 64 | let version = unsafe { cairo_version() }; 65 | println!("{}", version); 66 | } 67 | ``` 68 | 69 | Similarly you can invoke tools like pkg-config to retrieve cflags and pass them to bindgen: 70 | ```rust 71 | use c_import::c_import; 72 | 73 | c_import!( 74 | "", 75 | "", 76 | "$pkg-config --cflags cairo" 77 | ); 78 | 79 | fn main() { 80 | let version = unsafe { cairo_version() }; 81 | println!("{}", version); 82 | } 83 | ``` 84 | 85 | ## Macro parameters 86 | - Normal arguments are considered header files. 87 | - Arguments starting with `--link` are used to insert `#[link (name = libname)]` attributes to the generated extern functions, this allows linking the libraries without having to explicitly create a build.rs file containing `println!("cargo:rustc-link-lib=libname");` 88 | - Arguments starting with `--` are considered bindgen arguments. 89 | - Arguments starting with `-` are considered cflags, such as include paths or defines ("-I" & "-D" respectively). 90 | - Arguments starting with `$` are considered shell commands which return cflags, similar to pkg-config. 91 | 92 | ## Usage with C++ headers (limited) 93 | 94 | ```rust 95 | // src/main.rs 96 | use c_import::cpp_import; 97 | 98 | cpp_import!(""); 99 | 100 | fn main() { 101 | let version = unsafe { Fl::api_version() }; // static method of class Fl 102 | println!("{}", version); 103 | } 104 | ``` 105 | 106 | ```rust 107 | // build.rs 108 | fn main() { 109 | println!("cargo:rustc-link-lib=fltk"); 110 | } 111 | ``` 112 | 113 | Another example showing how to deal with C++ namespaces: 114 | 115 | ```cpp 116 | // src/my_header.hpp 117 | #pragma once 118 | 119 | namespace my_namespace { 120 | class MyStruct { 121 | int version_; 122 | public: 123 | MyStruct(int version); 124 | int version() const; 125 | }; 126 | } 127 | ``` 128 | 129 | ```rust 130 | // src/main.rs 131 | use c_import::cpp_import; 132 | 133 | cpp_import!("src/my_header.hpp"); 134 | 135 | fn main() { 136 | let h = unsafe { my_namespace_MyStruct::new(2) }; 137 | println!("{}", unsafe { h.version() }); 138 | } 139 | ``` 140 | 141 | 142 | ## Limitations 143 | - Mostly bindgen limitations: 144 | - with C++ headers. 145 | - with static inline functions. -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use std::hash::{Hash, Hasher}; 3 | use std::io::Write; 4 | use std::path::PathBuf; 5 | use std::process::Command; 6 | use std::time::{SystemTime, UNIX_EPOCH}; 7 | 8 | const FILESTEM: &str = "temphdr"; 9 | 10 | const BINDGEN_C_ARGS: &[&str] = &[ 11 | "--use-core", 12 | "--no-layout-tests", 13 | "--no-doc-comments", 14 | "--no-prepend-enum-name", 15 | "--disable-header-comment", 16 | ]; 17 | 18 | const BINDGEN_CPP_ARGS: &[&str] = &[ 19 | "--use-core", 20 | "--generate-inline-functions", 21 | "--no-layout-tests", 22 | "--no-doc-comments", 23 | "--no-prepend-enum-name", 24 | "--disable-header-comment", 25 | ]; 26 | 27 | const CLANG_C_ARGS: &[&str] = &[ 28 | "-std=c17", 29 | "-w", 30 | "-I.", 31 | ]; 32 | const CLANG_CPP_ARGS: &[&str] = &[ 33 | "-xc++", 34 | "-w", 35 | "-std=c++17", 36 | "-I.", 37 | ]; 38 | 39 | fn gen_header(input: String, is_cpp: bool) -> PathBuf { 40 | let mut hasher = std::collections::hash_map::DefaultHasher::new(); 41 | input.hash(&mut hasher); 42 | let input_hash = hasher.finish(); 43 | let now = SystemTime::now() 44 | .duration_since(UNIX_EPOCH) 45 | .expect("Failed to get current time!"); 46 | let filename = format!( 47 | "{:?}{}{}.{}", 48 | now.as_millis(), 49 | input_hash, 50 | FILESTEM, 51 | if is_cpp { "hpp" } else { "h" } 52 | ); 53 | let f = std::env::temp_dir().join(filename); 54 | let input = "#pragma once\n".to_string() + &input; 55 | std::fs::write(&f, &input).expect("Failed to generate temporary header!"); 56 | f 57 | } 58 | 59 | fn del_header(header: Option) { 60 | if let Some(f) = header { 61 | if f.exists() { 62 | std::fs::remove_file(f).expect("Failed to delete temporary header!"); 63 | } 64 | } 65 | } 66 | 67 | fn gen_command(header: String, bindgen_args: &[&str], clang_args: &[&str], is_cpp: bool) -> (Command, Option) { 68 | let mut cmd = Command::new("bindgen"); 69 | let (path, header) = { 70 | let header = gen_header(header, is_cpp); 71 | let path = format!("{}", header.display()); 72 | (path, Some(header)) 73 | }; 74 | cmd.arg(&path); 75 | cmd.args(bindgen_args); 76 | cmd.arg("--"); 77 | cmd.args(clang_args); 78 | (cmd, header) 79 | } 80 | 81 | fn run_cmd(cmd: &str) -> Vec { 82 | let v: Vec<&str> = cmd.split_whitespace().collect(); 83 | let mut cmd = Command::new(v[0]); 84 | cmd.args(&v[1..]); 85 | let cmd = cmd.output().expect("Failed to invoke command!"); 86 | String::from_utf8(cmd.stdout) 87 | .expect("Failed to parse output") 88 | .split_whitespace() 89 | .map(|s| s.to_string()) 90 | .collect() 91 | } 92 | 93 | pub(crate) fn common(input: TokenStream, is_cpp: bool) -> TokenStream { 94 | let input = input.to_string(); 95 | let input: Vec<&str> = input.split(',').collect(); 96 | let mut headers = vec![]; 97 | let mut extra_clang_args = vec![]; 98 | let mut extra_bindgen_args = vec![]; 99 | let mut links = vec![]; 100 | for elem in input { 101 | let elem = elem.trim(); 102 | if elem.starts_with("\"--link") { 103 | links.push(&elem[8..elem.len() - 1]); 104 | } else if elem.starts_with("\"--") { 105 | extra_bindgen_args.push(elem.to_string()); 106 | } else if elem.starts_with("\"-") { 107 | extra_clang_args.push(elem.to_string()); 108 | } else if elem.starts_with("\"<") { 109 | headers.push(&elem[1..elem.len() - 1]); 110 | } else if elem.starts_with("\"$") { 111 | let mut temp = run_cmd(&elem[2..elem.len() - 1]); 112 | extra_clang_args.append(&mut temp); 113 | } else { 114 | headers.push(elem); 115 | } 116 | } 117 | let extra_bindgen_args: Vec = extra_bindgen_args.iter().map(|s| s.replace('"', "")).collect(); 118 | let extra_clang_args: Vec = extra_clang_args.iter().map(|s| s.replace('"', "")).collect(); 119 | let mut bindgen_args: Vec<&str> = if is_cpp { 120 | BINDGEN_CPP_ARGS.to_vec() 121 | } else { 122 | BINDGEN_C_ARGS.to_vec() 123 | }; 124 | let mut clang_args: Vec<&str> = if is_cpp { 125 | CLANG_CPP_ARGS.to_vec() 126 | } else { 127 | CLANG_C_ARGS.to_vec() 128 | }; 129 | let header = headers 130 | .iter() 131 | .map(|s| format!("#include {}\n", s)) 132 | .collect(); 133 | bindgen_args.append(&mut extra_bindgen_args.iter().map(|s| s.trim()).collect()); 134 | clang_args.append(&mut extra_clang_args.iter().map(|s| s.trim()).collect()); 135 | let (mut cmd, header) = gen_command(header, &bindgen_args, &clang_args, is_cpp); 136 | let cmd = cmd.output().expect("Failed to invoke bindgen!"); 137 | del_header(header); 138 | if !cmd.status.success() { 139 | std::io::stderr().write_all(&cmd.stderr).unwrap(); 140 | } 141 | let mut output = String::from_utf8(cmd.stdout).expect("Failed to parse bindgen output"); 142 | if !links.is_empty() { 143 | output.push('\n'); 144 | for link in links { 145 | output.push_str(&format!("#[link(name = \"{}\")]", link)); 146 | } 147 | output.push_str("extern \"C\" {}"); 148 | } 149 | output 150 | .parse() 151 | .unwrap() 152 | } 153 | --------------------------------------------------------------------------------