#![warn(missing_docs)]
pub mod ast;
pub mod capture;
pub mod cfg;
pub mod codegen;
pub mod complex;
pub mod diag;
pub mod dump;
pub mod gnu;
pub mod include;
pub mod ir;
pub mod lex;
mod locate;
pub mod parse;
pub mod pp;
pub mod regions;
pub mod sema;
pub mod target;
use std::path::{Path, PathBuf};
use proc_macro2::{Ident, Literal, Span, TokenStream, TokenTree};
use quote::quote;
pub use ast::TranslationUnit;
pub use capture::{FileId, InputMode, Origin, Pos, Source, SourceMap, SourceRange, Subspan};
pub use diag::{Diagnostic, Diagnostics, Level};
pub use ir::{Program, Ty};
pub use pp::Token;
pub use target::{Arch, Env, Os, TargetModel, TargetSource, UnknownTarget};
pub const TARGET_ENV_VAR: &str = "CINRS_TARGET";
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Default)]
pub enum Standard {
C89,
#[default]
C99,
C11,
C17,
C23,
}
impl Standard {
pub fn as_str(self) -> &'static str {
match self {
Standard::C89 => "C89",
Standard::C99 => "C99",
Standard::C11 => "C11",
Standard::C17 => "C17",
Standard::C23 => "C23",
}
}
pub fn macro_name(self) -> &'static str {
self.macro_name_in(Dialect::Iso)
}
pub fn macro_name_in(self, dialect: Dialect) -> &'static str {
match (dialect, self) {
(Dialect::Iso, Standard::C89) => "c89!",
(Dialect::Gnu, Standard::C89) => "gnu89!",
(Dialect::Iso, Standard::C99) => "c99!",
(Dialect::Iso, Standard::C11) => "c11!",
(Dialect::Iso, Standard::C17) => "c17!",
(Dialect::Iso, Standard::C23) => "c23!",
(Dialect::Gnu, Standard::C99) => "gnu99!",
(Dialect::Gnu, Standard::C11) => "gnu11!",
(Dialect::Gnu, Standard::C17) => "gnu17!",
(Dialect::Gnu, Standard::C23) => "gnu23!",
}
}
pub fn macro_names_in(self, dialect: Dialect) -> &'static [&'static str] {
match (dialect, self) {
(Dialect::Iso, Standard::C89) => &["c89", "c90"],
(Dialect::Gnu, Standard::C89) => &["gnu89"],
(Dialect::Iso, Standard::C99) => &["c99"],
(Dialect::Iso, Standard::C11) => &["c11"],
(Dialect::Iso, Standard::C17) => &["c17"],
(Dialect::Iso, Standard::C23) => &["c23"],
(Dialect::Gnu, Standard::C99) => &["gnu99"],
(Dialect::Gnu, Standard::C11) => &["gnu11"],
(Dialect::Gnu, Standard::C17) => &["gnu17"],
(Dialect::Gnu, Standard::C23) => &["gnu23"],
}
}
pub fn include_macro_names_in(self, dialect: Dialect) -> &'static [&'static str] {
match (dialect, self) {
(Dialect::Iso, Standard::C89) => &["include_c89", "include_c90"],
(Dialect::Gnu, Standard::C89) => &["include_gnu89"],
(Dialect::Iso, Standard::C99) => &["include_c99"],
(Dialect::Iso, Standard::C11) => &["include_c11"],
(Dialect::Iso, Standard::C17) => &["include_c17"],
(Dialect::Iso, Standard::C23) => &["include_c23"],
(Dialect::Gnu, Standard::C99) => &["include_gnu99"],
(Dialect::Gnu, Standard::C11) => &["include_gnu11"],
(Dialect::Gnu, Standard::C17) => &["include_gnu17"],
(Dialect::Gnu, Standard::C23) => &["include_gnu23"],
}
}
pub fn requires(self, what: &str, needed: Standard) -> String {
format!(
"{what} requires {} or later (this block is {})",
needed.as_str(),
self.macro_name()
)
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum Dialect {
#[default]
Iso,
Gnu,
}
impl Dialect {
pub fn is_gnu(self) -> bool {
self == Dialect::Gnu
}
}
#[rustversion::since(1.99)]
pub const C_VARIADIC_SUPPORTED: bool = true;
#[rustversion::before(1.99)]
pub const C_VARIADIC_SUPPORTED: bool = false;
pub const COMPLEX_SUPPORTED: bool = cfg!(feature = "complex");
pub const COMPLEX_UNSUPPORTED: &str = "complex types are not supported here: '_Complex' needs the 'complex' feature of the \
cinrs crate, which is on by default and supplies the runtime the generated code links \
against";
#[derive(Clone, Debug)]
pub struct Options {
pub standard: Standard,
pub dialect: Dialect,
pub dollar_in_identifiers: bool,
pub include_paths: Vec<PathBuf>,
pub system_include: include::System,
pub target: TargetModel,
pub target_source: TargetSource,
pub c_variadic: bool,
pub complex: bool,
}
impl Default for Options {
fn default() -> Self {
Self::new(Standard::default())
}
}
impl Options {
pub fn new(standard: Standard) -> Self {
Self::with_dialect(standard, Dialect::Iso)
}
pub fn gnu(standard: Standard) -> Self {
Self::with_dialect(standard, Dialect::Gnu)
}
pub fn with_dialect(standard: Standard, dialect: Dialect) -> Self {
Self {
standard,
dialect,
dollar_in_identifiers: true,
include_paths: Vec::new(),
system_include: include::System::Off,
target: TargetModel::host(),
target_source: TargetSource::Host,
c_variadic: C_VARIADIC_SUPPORTED,
complex: COMPLEX_SUPPORTED,
}
}
pub fn with_complex(mut self, complex: bool) -> Self {
self.complex = complex;
self
}
pub fn for_target(mut self, target: TargetModel) -> Self {
self.target = target;
self.target_source = TargetSource::Explicit;
self
}
pub fn macro_name(&self) -> &'static str {
self.standard.macro_name_in(self.dialect)
}
pub fn origin(&self) -> Origin {
Origin::new(self.standard.macro_names_in(self.dialect))
}
pub fn include_origin(&self) -> Origin {
Origin::new(self.standard.include_macro_names_in(self.dialect))
}
pub fn gating(&self) -> Gating {
Gating {
standard: self.standard,
dialect: self.dialect,
}
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Gating {
pub standard: Standard,
pub dialect: Dialect,
}
impl Gating {
pub fn requires(self, what: &str, needed: Standard) -> Option<String> {
if self.dialect.is_gnu() || self.standard >= needed {
return None;
}
Some(format!(
"{what} requires {} or later (this block is {})",
needed.as_str(),
self.standard.macro_name_in(self.dialect)
))
}
pub fn implicit_int(self) -> bool {
self.standard < Standard::C99
}
pub fn implicit_function_declarations(self) -> bool {
self.standard < Standard::C99
}
pub fn old_style_definitions(self) -> bool {
self.standard < Standard::C23
}
pub fn newer_keyword(self, name: &str) -> Option<String> {
if self.dialect.is_gnu() {
return None;
}
let gnu = self.standard.macro_name_in(Dialect::Gnu);
let here = self.standard.macro_name_in(self.dialect);
match name {
"typeof" | "typeof_unqual" if self.standard < Standard::C23 => {
return Some(format!(
"'{name}' requires a GNU dialect ({gnu}) or C23 or later \
(this block is {here})"
));
}
"asm" => {
return Some(format!(
"'{name}' requires a GNU dialect ({gnu}); the spelling '__asm__' is \
available everywhere (this block is {here})"
));
}
_ => {}
}
let needed = crate::lex::Keyword::from_str(name, Standard::C23)?.since();
self.requires(&format!("'{name}'"), needed)
}
}
pub struct Analysis {
pub source: Source,
pub tokens: Vec<Token>,
pub unit: TranslationUnit,
pub diagnostics: Diagnostics,
pub expansions: pp::Expansions,
pub user_headers: Vec<PathBuf>,
pub embedded_files: Vec<PathBuf>,
pub link_libraries: Vec<String>,
pub safe_functions: Vec<pp::SafeName>,
pub export: bool,
pub no_std: bool,
pub crate_path: Option<String>,
pub options: Options,
}
const WORKER_STACK_SIZE: usize = 64 << 20;
fn on_large_stack<A, T>(arg: A, f: fn(A) -> T) -> T
where
A: Send + 'static,
T: Send + 'static,
{
use std::sync::{Arc, Mutex};
let cell = Arc::new(Mutex::new(Some(arg)));
let worker = Arc::clone(&cell);
let spawned = std::thread::Builder::new()
.name("cinrs-worker".to_owned())
.stack_size(WORKER_STACK_SIZE)
.spawn(move || {
let arg = worker
.lock()
.expect("the argument cell is never poisoned before use")
.take()
.expect("the argument is taken exactly once");
f(arg)
});
match spawned {
Ok(handle) => match handle.join() {
Ok(result) => result,
Err(payload) => std::panic::resume_unwind(payload),
},
Err(_) => {
let arg = cell
.lock()
.expect("the thread never started, so nothing poisoned the cell")
.take()
.expect("the argument is still there");
f(arg)
}
}
}
struct FrontEndInput {
ctx: pp::Context,
unit_range: SourceRange,
options: Options,
}
struct FrontEndOutput {
tokens: Vec<Token>,
unit: TranslationUnit,
diagnostics: Diagnostics,
expansions: pp::Expansions,
included: Vec<pp::IncludedFile>,
user_headers: Vec<PathBuf>,
embedded_files: Vec<PathBuf>,
link_libraries: Vec<String>,
safe_functions: Vec<pp::SafeName>,
export: bool,
no_std: bool,
crate_path: Option<String>,
options: Options,
}
fn front_end(input: FrontEndInput) -> FrontEndOutput {
let FrontEndInput {
mut ctx,
unit_range,
mut options,
} = input;
let mut diagnostics = Diagnostics::new();
let mut raw = lex::lex_text(&ctx.text, ctx.base, &(&options).into());
let (target_pragmas, relex) = pp::scan_target_pragma(&raw, &mut options, &mut diagnostics);
ctx.target_pragmas = target_pragmas;
if relex {
raw = lex::lex_text(&ctx.text, ctx.base, &(&options).into());
}
let pp::Preprocessed {
tokens,
expansions,
included,
user_headers,
embedded_files,
link_libraries,
safe_functions,
export,
no_std,
crate_path,
pack_events,
} = pp::preprocess(&raw, &ctx, &options, &mut diagnostics);
let packing = pp::PackMap::new(pack_events);
let unit = parse::parse(&tokens, unit_range, &packing, &options, &mut diagnostics);
expansions.annotate(&mut diagnostics);
FrontEndOutput {
tokens,
unit,
diagnostics,
expansions,
included,
user_headers,
embedded_files,
link_libraries,
safe_functions,
export,
no_std,
crate_path,
options,
}
}
pub fn analyze(input: TokenStream, options: &Options) -> Analysis {
analyze_with(input, options, &Origin::unknown())
}
pub fn analyze_with(input: TokenStream, options: &Options, origin: &Origin) -> Analysis {
let mut diagnostics = Diagnostics::new();
let source = capture::capture_with(input, &mut diagnostics, origin);
analyze_source(source, options, diagnostics)
}
fn analyze_source(mut source: Source, options: &Options, mut diagnostics: Diagnostics) -> Analysis {
let mut options = options.clone();
apply_env_target(&mut options, source.root_range(), &mut diagnostics);
apply_env_system_include(&mut options, source.root_range(), &mut diagnostics);
let file = source.map.file(source.root);
let arg = FrontEndInput {
ctx: pp::Context {
text: file.text().to_owned(),
base: file.base(),
file_name: file.rust_path().unwrap_or(pp::DEFAULT_FILE_NAME).to_owned(),
first_line: file.first_line(),
dir: including_directory(file.rust_path()),
next_base: source.map.next_base(),
target_pragmas: pp::TargetPragmas::default(),
},
unit_range: source.root_range(),
options,
};
let out = on_large_stack(arg, front_end);
for header in &out.included {
let span = source.map.span(header.directive);
let id = source
.map
.add_included_file(header.name.clone(), header.text.clone(), span);
debug_assert_eq!(
source.map.file(id).base(),
header.base,
"the preprocessor and the source map disagree about where '{}' starts",
header.name
);
}
diagnostics.extend(out.diagnostics);
Analysis {
source,
tokens: out.tokens,
unit: out.unit,
diagnostics,
expansions: out.expansions,
user_headers: out.user_headers,
embedded_files: out.embedded_files,
link_libraries: out.link_libraries,
safe_functions: out.safe_functions,
export: out.export,
no_std: out.no_std,
crate_path: out.crate_path,
options: out.options,
}
}
fn apply_env_target(options: &mut Options, range: SourceRange, diagnostics: &mut Diagnostics) {
if options.target_source != TargetSource::Host {
return;
}
let Ok(triple) = std::env::var(TARGET_ENV_VAR) else {
return;
};
let triple = triple.trim().to_owned();
if triple.is_empty() {
return;
}
let source = TargetSource::Env(triple);
match TargetModel::from_triple(source.triple().expect("Env carries its triple")) {
Ok(model) => {
options.target = model;
options.target_source = source;
}
Err(unknown) => diagnostics.error(range, unknown.message(&source)),
}
}
fn apply_env_system_include(
options: &mut Options,
range: SourceRange,
diagnostics: &mut Diagnostics,
) {
if options.system_include != include::System::Off {
return;
}
let Ok(value) = std::env::var(include::SYSTEM_ENV_VAR) else {
return;
};
match include::System::from_env_value(&value) {
Some(mode) => options.system_include = mode,
None => diagnostics.error(
range,
format!(
"{}={value:?} is not one of '1', 'first' or '0'",
include::SYSTEM_ENV_VAR
),
),
}
}
fn including_directory(rust_path: Option<&str>) -> Option<PathBuf> {
Some(Path::new(rust_path?).parent()?.to_path_buf())
}
pub fn expand_include(input: TokenStream, options: &Options) -> TokenStream {
let macro_name = format!("include_{}", options.macro_name());
let mut trees = input.into_iter();
let (first, second) = (trees.next(), trees.next());
let span = first.as_ref().map_or_else(Span::call_site, TokenTree::span);
let name = match (&first, &second) {
(Some(TokenTree::Literal(literal)), None) => capture::string_literal_value(literal),
_ => None,
};
let Some(name) = name else {
return diag::compile_error_at(
span,
&format!(
"{macro_name} takes one string literal naming a C file, as in \
{macro_name}(\"vendor/parser.c\")"
),
);
};
let path = include_path(&name, span, &first, &options.include_origin());
let found = match include::read_source(&path) {
Ok(found) => found,
Err(include::Error::Unreadable { path, error }) => {
return diag::compile_error_at(span, &format!("cannot read '{path}': {error}"));
}
Err(include::Error::NotFound { searched }) => {
let looked = searched.join(", ");
return diag::compile_error_at(
span,
&format!(
"{macro_name} cannot find '{name}': there is no file at {looked}. A relative \
path is resolved against the directory of the .rs file this macro is \
written in"
),
);
}
};
let tracked: Vec<PathBuf> = found.path.into_iter().collect();
let source = capture::capture_c_file(found.name, found.text, span);
let analysis = analyze_source(source, options, Diagnostics::new());
generate_unit(analysis, &tracked)
}
fn include_path(name: &str, span: Span, input: &Option<TokenTree>, origin: &Origin) -> PathBuf {
let path = Path::new(name);
if path.is_absolute() {
return path.to_path_buf();
}
if let Some(dir) = span
.local_file()
.and_then(|rs| rs.parent().map(Path::to_path_buf))
{
return dir.join(path);
}
if let Some(tree) = input
&& let Some(dir) =
capture::invocation_directory(tree, origin, |dir| dir.join(path).is_file())
{
return dir.join(path);
}
match std::env::var_os(include::MANIFEST_DIR_VAR) {
Some(root) => Path::new(&root).join(path),
None => path.to_path_buf(),
}
}
pub fn expand(input: TokenStream, options: &Options) -> TokenStream {
expand_with(input, options, &Origin::unknown())
}
pub fn expand_with(input: TokenStream, options: &Options, origin: &Origin) -> TokenStream {
generate_unit(analyze_with(input, options, origin), &[])
}
fn generate_unit(analysis: Analysis, extra_tracking: &[PathBuf]) -> TokenStream {
let Analysis {
source,
unit,
mut diagnostics,
expansions,
user_headers,
embedded_files,
link_libraries,
safe_functions,
export,
no_std,
crate_path,
options,
..
} = analysis;
let options = &options;
let unit_id = source.unit_id();
let (mut program, mut sema_diagnostics) = on_large_stack(
(unit, options.clone(), unit_id),
|(unit, options, unit_id)| sema::analyze(&unit, &options, unit_id),
);
expansions.annotate(&mut sema_diagnostics);
diagnostics.extend(sema_diagnostics);
program.link_libraries = link_libraries;
program.export = export;
program.no_std = no_std;
if let Some(path) = crate_path {
program.crate_path = path;
}
let mut pragma_diagnostics = sema::check_pragmas(&program);
pragma_diagnostics.extend(sema::check_safe(&mut program, &safe_functions));
expansions.annotate(&mut pragma_diagnostics);
diagnostics.extend(pragma_diagnostics);
let mut tracked = extra_tracking.to_vec();
tracked.extend(user_headers);
let mut out = rebuild_tracking(&tracked, &embedded_files);
if diagnostics.has_errors() {
out.extend(diagnostics.to_token_stream(&source.map));
out.extend(codegen::generate_stubs(&program, &source.map, options));
} else {
out.extend(codegen::generate(&program, &source.map, options));
}
in_module(out, unit_id)
}
fn in_module(items: TokenStream, unit_id: u64) -> TokenStream {
if items.is_empty() {
return items;
}
let span = Span::call_site();
let ident = Ident::new(&format!("__cinrs_unit_{:08x}", unit_id as u32), span);
quote! {
mod #ident {
#![allow(
unknown_lints,
arithmetic_overflow,
clashing_extern_declarations,
dead_code,
improper_ctypes,
improper_ctypes_definitions,
invalid_runtime_symbol_definitions,
non_camel_case_types,
non_snake_case,
non_upper_case_globals,
overflowing_literals,
static_mut_refs,
suspicious_runtime_symbol_definitions,
unconditional_panic,
unpredictable_function_pointer_comparisons,
unreachable_code,
unreachable_patterns,
unused_assignments,
unused_braces,
unused_comparisons,
unused_labels,
unused_mut,
unused_parens,
unused_unsafe,
unused_variables,
clippy::all
)]
#items
}
#[allow(unknown_lints, ambiguous_glob_reexports, unused_imports)]
pub use #ident::*;
}
}
fn rebuild_tracking(headers: &[PathBuf], embedded: &[PathBuf]) -> TokenStream {
let span = Span::call_site();
let mut out = TokenStream::new();
for header in headers {
let mut literal = Literal::string(&header.to_string_lossy());
literal.set_span(span);
let literal = TokenTree::Literal(literal);
out.extend(quote! {
const _: &::core::primitive::str = ::core::include_str!(#literal);
});
}
for resource in embedded {
let mut literal = Literal::string(&resource.to_string_lossy());
literal.set_span(span);
let literal = TokenTree::Literal(literal);
out.extend(quote! {
const _: &[::core::primitive::u8] = ::core::include_bytes!(#literal);
});
}
out
}