Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions compiler/rustc_attr_parsing/src/attributes/link_attrs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ use crate::diagnostics::{
AsNeededCompatibility, BothFfiConstAndPure, BundleNeedsStatic, EmptyLinkName,
ExportSymbolsNeedsStatic, ImportNameTypeRaw, ImportNameTypeX86, IncompatibleWasmLink,
InvalidLinkModifier, InvalidMachoSection, InvalidMachoSectionReason, LinkFrameworkApple,
LinkOrdinalOutOfRange, LinkRequiresName, MultipleModifiers, NullOnLinkName, NullOnLinkSection,
RawDylibOnlyWindows, WholeArchiveNeedsStatic,
LinkOrdinalOutOfRange, LinkRequiresName, LinkSectionForeignBpfOnly, MultipleModifiers,
NullOnLinkName, NullOnLinkSection, RawDylibOnlyWindows, WholeArchiveNeedsStatic,
};

pub(crate) struct LinkNameParser;
Expand Down Expand Up @@ -505,13 +505,22 @@ impl SingleAttributeParser for LinkSectionParser {
Allow(Target::Method(MethodKind::Inherent)),
Allow(Target::Method(MethodKind::Trait { body: true })),
Allow(Target::Method(MethodKind::TraitImpl)),
Allow(Target::ForeignStatic),
Allow(Target::ForeignFn),
Comment on lines +508 to +509

@bjorn3 bjorn3 Aug 20, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be gated to ebpf.

View changes since the review

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I've added a target gate in LinkSectionParser::convert() that rejects #[link_section] on ForeignStatic and ForeignFn when the target arch is not BPF following the same pattern as the import_name_type x86 gate a few lines above, let me know if you'd like me to follow a different approach!

]);
const TEMPLATE: AttributeTemplate = template!(
NameValueStr: "name",
"https://doc.rust-lang.org/reference/abi.html#the-link_section-attribute"
);

fn convert(cx: &mut AcceptContext<'_, '_>, args: &ArgParser) -> Option<AttributeKind> {
if matches!(cx.target, Target::ForeignStatic | Target::ForeignFn)
&& cx.sess.target.arch != Arch::Bpf
{
cx.emit_err(LinkSectionForeignBpfOnly { span: cx.attr_span });
return None;
}

let nv = cx.expect_name_value(args, cx.attr_span, None)?;
let name = cx.expect_string_literal(nv)?;
if name.as_str().contains('\0') {
Expand Down
7 changes: 7 additions & 0 deletions compiler/rustc_attr_parsing/src/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1235,6 +1235,13 @@ pub(crate) struct NullOnLinkSection {
pub span: Span,
}

#[derive(Diagnostic)]
#[diag("`link_section` on foreign items is only supported on BPF targets")]
pub(crate) struct LinkSectionForeignBpfOnly {
#[primary_span]
pub span: Span,
}

#[derive(Diagnostic)]
#[diag("link name may not contain null characters", code = E0648)]
pub(crate) struct NullOnLinkName {
Expand Down
8 changes: 8 additions & 0 deletions compiler/rustc_codegen_llvm/src/callee.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use rustc_middle::ty::{self, Instance, TypeVisitableExt};
use rustc_target::spec::{Arch, Env};
use tracing::debug;

use crate::base;
use crate::context::CodegenCx;
use crate::llvm::{self, Value};

Expand Down Expand Up @@ -152,6 +153,13 @@ pub(crate) fn get_fn<'ll, 'tcx>(cx: &CodegenCx<'ll, 'tcx>, instance: Instance<'t

cx.assume_dso_local(llfn, true);

if tcx.is_foreign_item(instance_def_id) {
base::set_link_section(llfn, tcx.codegen_fn_attrs(instance_def_id));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quoted LLM-generated analysis (Codex):

If two compatible extern declarations name the same function, and the unannotated declaration is referenced before the declaration carrying #[link_section = ".ksyms"], get_declared_value(sym) reuses the first LLVM symbol and skips this block for the second declaration.

The section attribute consequently depends on reference order. LLVM only adds the external function's DATASEC entry when the function has a section, so the annotated declaration can still produce a function without its required .ksyms entry.

if tcx.sess.target.arch == Arch::Bpf {
cx.dbg_scope_foreign_fn(instance, fn_abi, Some(llfn));
}
}

llfn
};

Expand Down
7 changes: 7 additions & 0 deletions compiler/rustc_codegen_llvm/src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,13 @@ impl<'ll> CodegenCx<'ll, '_> {
llvm::set_dllimport_storage_class(g);
}

if self.tcx.is_foreign_item(def_id) {
base::set_link_section(g, fn_attrs);
if self.tcx.sess.target.arch == Arch::Bpf {
debuginfo::build_extern_static_di_node(self, def_id, g);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quoted LLM-generated analysis (Codex):

With #[linkage = "extern_weak"], check_and_apply_linkage returns Rust's synthetic internal pointer global, not the external symbol. This block therefore attaches .ksyms and debug metadata to the wrapper, leaving the actual weak symbol without matching BTF.

LLVM skips the external global without metadata and classifies the initialized internal wrapper as VAR_STATIC, regardless of DIGlobalVariable.isDefinition. Nullable weak function imports have the same problem: their native declaration is an LLVM function, but only the pointer wrapper receives global-variable metadata.

}
}

self.instances.borrow_mut().insert(instance, g);
g
}
Expand Down
7 changes: 5 additions & 2 deletions compiler/rustc_codegen_llvm/src/debuginfo/di_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ pub(crate) trait DIBuilderExt<'ll> {
unsafe { llvm::LLVMDIBuilderCreateExpression(this, addr_ops.as_ptr(), addr_ops.len()) }
}

/// Creates a DIGlobalVariable debug info node.
fn create_static_variable(
&self,
scope: Option<&'ll llvm::Metadata>,
Expand All @@ -52,21 +53,22 @@ pub(crate) trait DIBuilderExt<'ll> {
line_number: c_uint,
ty: &'ll llvm::Metadata,
is_local_to_unit: bool,
is_definition: bool,
val: &'ll llvm::Value,
decl: Option<&'ll llvm::Metadata>,
align: Option<Align>,
) -> &'ll llvm::Metadata {
let this = self.as_di_builder();
let align_in_bits = align.map_or(0, |align| align.bits() as u32);

// `LLVMDIBuilderCreateGlobalVariableExpression` would assert if we
// `LLVMRustDIBuilderCreateGlobalVariableExpression` would assert if we
// gave it a null `Expr` pointer, so give it an empty expression
// instead, which is what the C++ `createGlobalVariableExpression`
// method would do if given a null `DIExpression` pointer.
let expr = self.create_expression(&[]);

let global_var_expr = unsafe {
llvm::LLVMDIBuilderCreateGlobalVariableExpression(
llvm::LLVMRustDIBuilderCreateGlobalVariableExpression(
this,
scope,
name.as_ptr(),
Expand All @@ -77,6 +79,7 @@ pub(crate) trait DIBuilderExt<'ll> {
line_number,
ty,
is_local_to_unit.to_llvm_bool(),
is_definition.to_llvm_bool(),
expr,
decl,
align_in_bits,
Expand Down
44 changes: 30 additions & 14 deletions compiler/rustc_codegen_llvm/src/debuginfo/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1462,43 +1462,57 @@ fn build_generic_type_param_di_nodes<'ll, 'tcx>(
}
}

/// Creates debug information for the given global variable.
/// Creates debug information for the given global variable (definition).
///
/// Adds the created debuginfo nodes directly to the crate's IR.
pub(crate) fn build_global_var_di_node<'ll>(
cx: &CodegenCx<'ll, '_>,
def_id: DefId,
global: &'ll Value,
) {
let DefKind::Static { nested, .. } = cx.tcx.def_kind(def_id) else { bug!() };
if nested {
return;
}

let is_local_to_unit = is_node_local_to_unit(cx, def_id);
build_static_var_di_node_inner(cx, def_id, global, is_local_to_unit, true);
}

/// Creates debug information for a foreign static (declaration, not definition).
pub(crate) fn build_extern_static_di_node<'ll>(
cx: &CodegenCx<'ll, '_>,
def_id: DefId,
global: &'ll Value,
) {
build_static_var_di_node_inner(cx, def_id, global, false, false);
}

fn build_static_var_di_node_inner<'ll>(
cx: &CodegenCx<'ll, '_>,
def_id: DefId,
global: &'ll Value,
is_local_to_unit: bool,
is_definition: bool,
) {
if cx.dbg_cx.is_none() {
return;
}

// Only create type information if full debuginfo is enabled
if cx.sess().opts.debuginfo != DebugInfo::Full {
return;
}

let tcx = cx.tcx;

// We may want to remove the namespace scope if we're in an extern block (see
// https://github.com/rust-lang/rust/pull/46457#issuecomment-351750952).
let var_scope = get_namespace_for_item(cx, def_id);
let (file_metadata, line_number) = file_metadata_from_def_id(cx, Some(def_id));

let is_local_to_unit = is_node_local_to_unit(cx, def_id);

let DefKind::Static { nested, .. } = cx.tcx.def_kind(def_id) else { bug!() };
if nested {
return;
}
let variable_type = Instance::mono(cx.tcx, def_id).ty(cx.tcx, cx.typing_env());
let type_di_node = type_di_node(cx, variable_type);
let var_name = tcx.item_name(def_id);
let var_name = var_name.as_str();
let linkage_name = mangled_name_of_instance(cx, Instance::mono(tcx, def_id)).name;
// When empty, linkage_name field is omitted,
// which is what we want for no_mangle statics
let linkage_name = if var_name == linkage_name { "" } else { linkage_name };

let global_align = cx.align_of(variable_type);
Expand All @@ -1511,8 +1525,9 @@ pub(crate) fn build_global_var_di_node<'ll>(
line_number,
type_di_node,
is_local_to_unit,
global, // (value)
None, // (decl)
is_definition,
global,
None,
Some(global_align),
);
}
Expand Down Expand Up @@ -1789,6 +1804,7 @@ pub(crate) fn create_vtable_di_node<'ll, 'tcx>(
UNKNOWN_LINE_NUMBER,
vtable_type_di_node,
true, // (is_local_to_unit)
true, // (is_definition)
vtable, // (value)
None, // (decl)
None::<Align>,
Expand Down
89 changes: 88 additions & 1 deletion compiler/rustc_codegen_llvm/src/debuginfo/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ use smallvec::SmallVec;
use tracing::debug;

pub(crate) use self::di_builder::DIBuilderExt;
pub(crate) use self::metadata::build_global_var_di_node;
use self::metadata::{
UNKNOWN_COLUMN_NUMBER, UNKNOWN_LINE_NUMBER, file_metadata, spanned_type_di_node, type_di_node,
};
pub(crate) use self::metadata::{build_extern_static_di_node, build_global_var_di_node};
use self::namespace::mangled_name_of_instance;
use self::utils::{DIB, create_DIArray, is_node_local_to_unit};
use crate::builder::Builder;
Expand Down Expand Up @@ -755,3 +755,90 @@ impl<'ll, 'tcx> DebugInfoCodegenMethods<'tcx> for CodegenCx<'ll, 'tcx> {
metadata::create_vtable_di_node(self, ty, trait_ref, vtable)
}
}

impl<'ll, 'tcx> CodegenCx<'ll, 'tcx> {
/// Creates a `DISubprogram` for a foreign function declaration (without `SPFlagDefinition`).
///
/// This is gated to BPF targets and emits the debug info that LLVM's BPF backend
/// needs to generate BTF FUNC entries for kfunc resolution.
pub(crate) fn dbg_scope_foreign_fn(
&self,
instance: Instance<'tcx>,
fn_abi: &FnAbi<'tcx, Ty<'tcx>>,
llfn: Option<&'ll Value>,
) {
if self.dbg_cx.is_none() {
return;
}

if self.sess().opts.debuginfo != DebugInfo::Full {
return;
}

let tcx = self.tcx;
let def_id = instance.def_id();

let scope = namespace::item_namespace(
self,
DefId {
krate: def_id.krate,
index: tcx.def_key(def_id).parent.expect("dbg_scope_foreign_fn: missing parent?"),
},
);

let span = tcx.def_span(def_id);
let loc = self.lookup_debug_loc(span.lo());
let file_metadata = file_metadata(self, &loc.file);

let signature: Vec<_> = iter::once(if fn_abi.ret.is_ignore() {
None
} else {
Some(type_di_node(self, fn_abi.ret.layout.ty))
})
.chain(fn_abi.args.iter().map(|arg| Some(type_di_node(self, arg.layout.ty))))
.collect();

let function_type_metadata = create_subroutine_type(self, &signature);

let mut name = String::with_capacity(64);
type_names::push_item_name(tcx, def_id, false, &mut name);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quoted LLM-generated analysis (Codex):

For #[link_name = "bpf_task_acquire"] fn acquire(...), the new metadata names the function acquire, while the LLVM symbol is bpf_task_acquire.

LLVM's BTF generator uses SP->getName() and ignores linkageName. Libbpf matches the BTF name against the ELF symbol, so it cannot resolve this extern.


let linkage_name = &mangled_name_of_instance(self, instance).name;
let linkage_name = if &name == linkage_name { "" } else { linkage_name };

let scope_line = loc.line;

let mut flags = DIFlags::FlagPrototyped;
if fn_abi.ret.layout.is_uninhabited() {
flags |= DIFlags::FlagNoReturn;
}

// No SPFlagDefinition -- this is a declaration only.
let mut spflags = DISPFlags::SPFlagZero;
if self.sess().opts.optimize != config::OptLevel::No {
spflags |= DISPFlags::SPFlagOptimized;
}

let template_parameters = create_DIArray(DIB(self), &[]);

unsafe {
llvm::LLVMRustDIBuilderCreateFunction(
DIB(self),
scope,
name.as_c_char_ptr(),
name.len(),
linkage_name.as_c_char_ptr(),
linkage_name.len(),
file_metadata,
loc.line,
function_type_metadata,
scope_line,
flags,
spflags,
llfn,
template_parameters,
None,
);
}
}
}
3 changes: 2 additions & 1 deletion compiler/rustc_codegen_llvm/src/llvm/ffi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1901,7 +1901,7 @@ unsafe extern "C" {
Length: size_t,
) -> &'ll Metadata;

pub(crate) fn LLVMDIBuilderCreateGlobalVariableExpression<'ll>(
pub(crate) fn LLVMRustDIBuilderCreateGlobalVariableExpression<'ll>(
Builder: &DIBuilder<'ll>,
Scope: Option<&'ll Metadata>,
Name: *const c_uchar, // See "PTR_LEN_STR".
Expand All @@ -1912,6 +1912,7 @@ unsafe extern "C" {
LineNo: c_uint,
Ty: &'ll Metadata,
LocalToUnit: llvm::Bool,
IsDefined: llvm::Bool,
Expr: &'ll Metadata,
Decl: Option<&'ll Metadata>,
AlignInBits: u32,
Expand Down
16 changes: 16 additions & 0 deletions compiler/rustc_llvm/llvm-wrapper/RustWrapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1071,6 +1071,22 @@ extern "C" LLVMMetadataRef LLVMRustDIBuilderCreateMethod(
return wrap(Sub);
}

// Wraps DIBuilder::createGlobalVariableExpression. Unlike the LLVM-C API
// (LLVMDIBuilderCreateGlobalVariableExpression), this exposes the IsDefined
// parameter instead of hard-coding it to true.
extern "C" LLVMMetadataRef LLVMRustDIBuilderCreateGlobalVariableExpression(
LLVMDIBuilderRef Builder, LLVMMetadataRef Scope, const char *Name,
size_t NameLen, const char *Linkage, size_t LinkLen, LLVMMetadataRef File,
unsigned LineNo, LLVMMetadataRef Ty, LLVMBool LocalToUnit,
LLVMBool IsDefined, LLVMMetadataRef Expr, LLVMMetadataRef Decl,
uint32_t AlignInBits) {
return wrap(unwrap(Builder)->createGlobalVariableExpression(
unwrapDI<DIScope>(Scope), {Name, NameLen}, {Linkage, LinkLen},
unwrapDI<DIFile>(File), LineNo, unwrapDI<DIType>(Ty), LocalToUnit,
IsDefined, unwrap<DIExpression>(Expr), unwrapDI<MDNode>(Decl), nullptr,
AlignInBits));
}

extern "C" LLVMMetadataRef LLVMRustDIBuilderCreateVariantPart(
LLVMDIBuilderRef Builder, LLVMMetadataRef Scope, const char *Name,
size_t NameLen, LLVMMetadataRef File, unsigned LineNumber,
Expand Down
35 changes: 35 additions & 0 deletions tests/codegen-llvm/bpf-extern-debuginfo.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Checks that BPF extern declarations are emitted as debug info declarations.
//
//@ only-bpf

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quoted LLM-generated analysis (Codex):

only-bpf is evaluated against compiletest's suite target, not this test's --target flag. Normal x86_64/aarch64 CI therefore skips this test and link-section-foreign.rs.

Removing the filter alone is insufficient: these fixtures require BPF core, which the host-target suite does not build. The existing bpf-alu32.rs uses add-minicore/no_core with a target override instead.

//@ needs-llvm-components: bpf
//@ compile-flags: --target bpfel-unknown-none -C debuginfo=2

#![no_std]
#![no_main]
#![crate_type = "lib"]

extern "C" {
// CHECK: !DIGlobalVariable(name: "KERNEL_VERSION"
// CHECK-SAME: isLocal: false
// CHECK-SAME: isDefinition: false
#[link_section = ".ksyms"]
pub static KERNEL_VERSION: u64;
}

extern "C" {
// CHECK: !DISubprogram(name: "bpf_kfunc"
// CHECK-SAME: flags: DIFlagPrototyped
// CHECK-NOT: DISPFlagDefinition
#[link_section = ".ksyms"]
pub fn bpf_kfunc(x: u64) -> u64;
}

#[no_mangle]
pub fn test_extern_items() -> u64 {
unsafe { KERNEL_VERSION + bpf_kfunc(42) }
}

#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
loop {}
}
Loading
Loading