blob: e9243389f93e332767fa017bef9dc0e791bd1148 [file] [log] [blame]
// Part of the Crubit project, under the Apache License v2.0 with LLVM
// Exceptions. See /LICENSE for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
use anyhow::{bail, Result};
use code_gen_utils::format_cc_ident;
use proc_macro2::TokenStream;
use quote::quote;
use rustc_interface::Queries;
use rustc_middle::dep_graph::DepContext;
use rustc_middle::middle::exported_symbols::ExportedSymbol;
use rustc_middle::ty::TyCtxt;
use rustc_span::def_id::{LocalDefId, LOCAL_CRATE};
use std::fmt::Display;
pub struct GeneratedBindings {
pub h_body: TokenStream,
}
impl GeneratedBindings {
pub fn generate(tcx: TyCtxt) -> Self {
let top_comment = {
let crate_name = tcx.crate_name(LOCAL_CRATE);
let txt = format!(
"Automatically @generated C++ bindings for the following Rust crate:\n\
{crate_name}"
);
quote! { __COMMENT__ #txt __NEWLINE__ }
};
let h_body = {
let crate_content = format_crate(tcx).unwrap_or_else(|err| {
let txt = format!("Failed to generate bindings for the crate: {}", err);
quote! { __COMMENT__ #txt }
});
// TODO(b/251445877): Replace `#pragma once` with include guards.
quote! {
#top_comment
__HASH_TOKEN__ pragma once __NEWLINE__
__NEWLINE__
#crate_content
}
};
Self { h_body }
}
}
/// Helper (used by `bindings_driver` and `test::run_compiler`) for invoking
/// functions operating on `TyCtxt`.
pub fn enter_tcx<'tcx, F, T>(
queries: &'tcx Queries<'tcx>,
f: F,
) -> rustc_interface::interface::Result<T>
where
F: FnOnce(TyCtxt<'tcx>) -> T + Send,
T: Send,
{
let query_context = queries.global_ctxt()?;
Ok(query_context.peek_mut().enter(f))
}
fn format_def(_tcx: TyCtxt, _def_id: LocalDefId) -> Result<TokenStream> {
bail!("Nothing works yet!")
}
fn format_unsupported_def(
tcx: TyCtxt,
local_def_id: LocalDefId,
err_msg: impl Display,
) -> TokenStream {
let span = tcx.sess().source_map().span_to_embeddable_string(tcx.def_span(local_def_id));
let name = tcx.def_path_str(local_def_id.to_def_id());
let msg = format!("Error while generating bindings for `{name}` defined at {span}: {err_msg}");
quote! { __NEWLINE__ __NEWLINE__ __COMMENT__ #msg __NEWLINE__ }
}
fn format_crate(tcx: TyCtxt) -> Result<TokenStream> {
let crate_name = format_cc_ident(tcx.crate_name(LOCAL_CRATE).as_str())?;
// TODO(lukasza): We probably shouldn't be using `exported_symbols` as the main
// entry point for finding Rust definitions that need to be wrapping in C++
// bindings. For example, it _seems_ that things like `type` aliases or
// `struct`s (without an `impl`) won't be visible to a linker and therefore
// won't have exported symbols.
let snippets =
tcx.exported_symbols(LOCAL_CRATE).iter().filter_map(move |(symbol, _)| match symbol {
ExportedSymbol::NonGeneric(def_id) => {
// It seems that non-generic exported symbols should all be defined in the
// `LOCAL_CRATE`. Furthermore, `def_id` seems to be a `LocalDefId`. OTOH, it
// isn't clear why `ExportedSymbol::NonGeneric` holds a `DefId` rather than a
// `LocalDefId`. For now, we assert `expect_local` below (and if it fails, then
// hopefully it will help us understand these things better and maybe add
// extra unit tests against out code).
let local_id = def_id.expect_local();
Some(match format_def(tcx, local_id) {
Ok(snippet) => snippet,
Err(err) => format_unsupported_def(tcx, local_id, err),
})
}
ExportedSymbol::Generic(def_id, _substs) => {
// Ignore non-local defs. Map local defs to an unsupported comment.
//
// We are guessing that a non-local `def_id` can happen when the `LOCAL_CRATE`
// exports a monomorphization/specialization of a generic defined in a different
// crate. One specific example (covered via `async fn` in one of the tests) is
// `DefId(2:14250 ~ core[ef75]::future::from_generator)`.
def_id.as_local().map(|local_id| {
format_unsupported_def(tcx, local_id, "Generics are not supported yet.")
})
}
ExportedSymbol::DropGlue(_) | ExportedSymbol::NoDefId(_) => None,
});
Ok(quote! {
namespace #crate_name {
#( #snippets )*
}
})
}
#[cfg(test)]
pub mod tests {
use super::{format_def, GeneratedBindings};
use anyhow::Result;
use itertools::Itertools;
use proc_macro2::TokenStream;
use quote::quote;
use rustc_middle::ty::TyCtxt;
use rustc_span::def_id::LocalDefId;
use std::path::PathBuf;
use token_stream_matchers::{assert_cc_matches, assert_cc_not_matches};
pub fn get_sysroot_for_testing() -> PathBuf {
let runfiles = runfiles::Runfiles::create().unwrap();
runfiles.rlocation(if std::env::var("LEGACY_TOOLCHAIN_RUST_TEST").is_ok() {
"google3/third_party/unsupported_toolchains/rust/toolchains/nightly"
} else {
"google3/nowhere/llvm/rust"
})
}
#[test]
#[should_panic(expected = "Test inputs shouldn't cause compilation errors")]
fn test_infra_panic_when_test_input_contains_syntax_errors() {
run_compiler("syntax error here", |_tcx| panic!("This part shouldn't execute"))
}
#[test]
#[should_panic(expected = "Test inputs shouldn't cause compilation errors")]
fn test_infra_panic_when_test_input_triggers_analysis_errors() {
run_compiler("#![feature(no_such_feature)]", |_tcx| panic!("This part shouldn't execute"))
}
#[test]
#[should_panic(expected = "Test inputs shouldn't cause compilation errors")]
fn test_infra_panic_when_test_input_triggers_warnings() {
run_compiler("pub fn foo(unused_parameter: i32) {}", |_tcx| {
panic!("This part shouldn't execute")
})
}
#[test]
fn test_infra_nightly_features_ok_in_test_input() {
// This test arbitrarily picks `yeet_expr` as an example of a feature that
// hasn't yet been stabilized.
let test_src = r#"
// This test is supposed to test that *nightly* features are ok
// in the test input. The `forbid` directive below helps to
// ensure that we'll realize in the future when the `yeet_expr`
// feature gets stabilized, making it not quite fitting for use
// in this test.
#![forbid(stable_features)]
#![feature(yeet_expr)]
"#;
run_compiler(test_src, |_tcx| ())
}
#[test]
fn test_infra_stabilized_features_ok_in_test_input() {
// This test arbitrarily picks `const_ptr_offset_from` as an example of a
// feature that has been already stabilized.
run_compiler("#![feature(const_ptr_offset_from)]", |_tcx| ())
}
#[test]
#[should_panic(expected = "No items named `missing_name`.\n\
Instead found:\n`bar`,\n`foo`,\n`m1`,\n`m2`,\n`std`")]
fn test_find_def_id_by_name_panic_when_no_item_with_matching_name() {
let test_src = r#"
pub extern "C" fn foo() {}
pub mod m1 {
pub fn bar() {}
}
pub mod m2 {
pub fn bar() {}
}
"#;
run_compiler(test_src, |tcx| find_def_id_by_name(tcx, "missing_name"));
}
#[test]
#[should_panic(expected = "More than one item named `some_name`")]
fn test_find_def_id_by_name_panic_when_multiple_items_with_matching_name() {
let test_src = r#"
pub mod m1 {
pub fn some_name() {}
}
pub mod m2 {
pub fn some_name() {}
}
"#;
run_compiler(test_src, |tcx| find_def_id_by_name(tcx, "some_name"));
}
#[test]
fn test_generated_bindings_fn_success() {
// This test covers only a single example of a function that should get a C++
// binding. Additional coverage of how items are formatted is provided by
// `test_format_def_...` tests.
let test_src = r#"
pub extern "C" fn public_function() {
println!("foo");
}
"#;
test_generated_bindings(test_src, |bindings| {
// TODO(lukasza): Fix test expectations once this becomes supported (in early Q4
// 2022).
let expected_comment_txt = "Error while generating bindings for `public_function` \
defined at <crubit_unittests.rs>:2:17: 2:52: \
Nothing works yet!";
assert_cc_matches!(
bindings.h_body,
quote! {
__COMMENT__ #expected_comment_txt
}
);
});
}
#[test]
fn test_generated_bindings_fn_non_pub() {
let test_src = r#"
#![allow(dead_code)]
extern "C" fn private_function() {
println!("foo");
}
"#;
test_generated_bindings(test_src, |bindings| {
// Non-public functions should not be present in the generated bindings.
assert_cc_not_matches!(bindings.h_body, quote! { private_function });
});
}
#[test]
fn test_generated_bindings_top_level_items() {
let test_src = "pub fn public_function() {}";
test_generated_bindings(test_src, |bindings| {
let expected_comment_txt =
"Automatically @generated C++ bindings for the following Rust crate:\n\
rust_out";
assert_cc_matches!(
bindings.h_body,
quote! {
__COMMENT__ #expected_comment_txt
...
__HASH_TOKEN__ pragma once
...
namespace rust_out {
...
}
}
);
})
}
#[test]
fn test_generated_bindings_unsupported_item() {
// This test verifies how `Err` from `format_def` is formatted as a C++ comment
// (in `format_crate` and `format_unsupported_def`).
// - This test covers only a single example of an unsupported item. Additional
// coverage is provided by `test_format_def_unsupported_...` tests.
// - This test somewhat arbitrarily chooses an example of an unsupported item
// (i.e. if `async fn` becomes supported by `cc_bindings_from_rs` in the
// future, then the test will have to be modified to use another `test_src`
// input).
let test_src = r#"
pub async fn public_function() {}
"#;
test_generated_bindings(test_src, |bindings| {
let expected_comment_txt = "Error while generating bindings for `public_function` \
defined at <crubit_unittests.rs>:2:17: 2:47: \
Nothing works yet!";
assert_cc_matches!(
bindings.h_body,
quote! {
__COMMENT__ #expected_comment_txt
}
);
})
}
#[test]
fn test_format_def_unsupported_fn_extern_c_no_params_no_return_type() {
let test_src = r#"
pub extern "C" fn public_function() {}
"#;
test_format_def(test_src, "public_function", |result| {
// TODO(lukasza): Fix test expectations once this becomes supported (in early Q4
// 2022).
let err = result.expect_err("Test expects an error here").to_string();
assert_eq!(err, "Nothing works yet!");
});
}
#[test]
fn test_format_def_unsupported_fn_async() {
let test_src = r#"
pub async fn public_function() {}
"#;
test_format_def(test_src, "public_function", |result| {
let err = result.expect_err("Test expects an error here").to_string();
assert_eq!(err, "Nothing works yet!");
});
}
/// Tests invoking `format_def` on the item with the specified `name` from
/// the given Rust `source`. Returns the result of calling
/// `test_function` with `format_def`'s result as an argument.
/// (`test_function` should typically `assert!` that it got the expected
/// result from `format_def`.)
fn test_format_def<F, T>(source: &str, name: &str, test_function: F) -> T
where
F: FnOnce(Result<TokenStream>) -> T + Send,
T: Send,
{
run_compiler(source, |tcx| {
let def_id = find_def_id_by_name(tcx, name);
test_function(format_def(tcx, def_id))
})
}
/// Finds the definition id of a Rust item with the specified `name`.
/// Panics if no such item is found, or if there is more than one match.
fn find_def_id_by_name(tcx: TyCtxt, name: &str) -> LocalDefId {
let hir_items = || tcx.hir().items().map(|item_id| tcx.hir().item(item_id));
let items_with_matching_name =
hir_items().filter(|item| item.ident.name.as_str() == name).collect_vec();
match items_with_matching_name.as_slice() {
&[] => {
let found_names = hir_items()
.map(|item| item.ident.name.as_str())
.filter(|s| !s.is_empty())
.sorted()
.dedup()
.map(|name| format!("`{name}`"))
.collect_vec();
panic!("No items named `{}`.\nInstead found:\n{}", name, found_names.join(",\n"));
}
&[item] => item.def_id.def_id,
_ => panic!("More than one item named `{name}`"),
}
}
/// Tests invoking `GeneratedBindings::generate` on the given Rust `source`.
/// Returns the result of calling `test_function` with the generated
/// bindings as an argument. (`test_function` should typically `assert!`
/// that it got the expected `GeneratedBindings`.)
fn test_generated_bindings<F, T>(source: &str, test_function: F) -> T
where
F: FnOnce(GeneratedBindings) -> T + Send,
T: Send,
{
run_compiler(source, |tcx| test_function(GeneratedBindings::generate(tcx)))
}
/// Invokes the Rust compiler on the given Rust `source` and then calls `f`
/// on the `TyCtxt` representation of the compiled `source`.
fn run_compiler<F, T>(source: impl Into<String>, f: F) -> T
where
F: for<'tcx> FnOnce(TyCtxt<'tcx>) -> T + Send,
T: Send,
{
use rustc_session::config::{CrateType, Input, Options, OutputType, OutputTypes};
const TEST_FILENAME: &str = "crubit_unittests.rs";
// Setting `output_types` that will trigger code gen - otherwise some parts of
// the analysis will be missing (e.g. `tcx.exported_symbols()`).
// The choice of `Bitcode` is somewhat arbitrary (e.g. `Assembly`,
// `Mir`, etc. would also trigger code gen).
let output_types = OutputTypes::new(&[(OutputType::Bitcode, None /* PathBuf */)]);
let opts = Options {
crate_types: vec![CrateType::Rlib], // Test inputs simulate library crates.
maybe_sysroot: Some(get_sysroot_for_testing()),
output_types,
edition: rustc_span::edition::Edition::Edition2021,
unstable_features: rustc_feature::UnstableFeatures::Allow,
lint_opts: vec![
("warnings".to_string(), rustc_lint_defs::Level::Deny),
("stable_features".to_string(), rustc_lint_defs::Level::Allow),
],
..Default::default()
};
let config = rustc_interface::interface::Config {
opts,
crate_cfg: Default::default(),
crate_check_cfg: Default::default(),
input: Input::Str {
name: rustc_span::FileName::Custom(TEST_FILENAME.to_string()),
input: source.into(),
},
input_path: None,
output_file: None,
output_dir: None,
file_loader: None,
diagnostic_output: rustc_session::DiagnosticOutput::Default,
lint_caps: Default::default(),
parse_sess_created: None,
register_lints: None,
override_queries: None,
make_codegen_backend: None,
registry: rustc_errors::registry::Registry::new(rustc_error_codes::DIAGNOSTICS),
};
rustc_interface::interface::run_compiler(config, |compiler| {
compiler.enter(|queries| {
use rustc_interface::interface::Result;
let result: Result<Result<()>> = super::enter_tcx(queries, |tcx| {
// Explicitly force full `analysis` stage to detect compilation
// errors that the earlier stages might miss. This helps ensure that the
// test inputs are valid Rust (even if `f` wouldn't
// have triggered full analysis).
tcx.analysis(())
});
// Flatten the outer and inner results into a single result. (outer result
// comes from `enter_tcx`; inner result comes from `analysis`).
//
// TODO(lukasza): Use `Result::flatten` API when it gets stabilized. See also
// https://github.com/rust-lang/rust/issues/70142
let result: Result<()> = result.and_then(|result| result);
// `analysis` might succeed even if there are some lint / warning errors.
// Detecting these requires explicitly checking `compile_status`.
let result: Result<()> = result.and_then(|()| compiler.session().compile_status());
// Run the provided callback.
let result: Result<T> = result.and_then(|()| super::enter_tcx(queries, f));
result.expect("Test inputs shouldn't cause compilation errors")
})
})
}
}