Right. I'm not saying that accesses to structures nested by value are uncommon. What I'm saying is that we could roll out experimental nightly-only support for BTF relocations, behind a feature gate, without waiting for const Sized / sized hierarchy to be fully implemented.
That said, I agree that sized hierarchy and support for accesses nested by value would definitely be needed to expose the feature without a gate and on stable Rust.
The more I think about it, the more I'm leaning towards neutral naming not tied to BTF, which could also suit Swift ABI or any other kind of field access relocation. My current implementation touches the field projection logic (compiler/rustc_codegen_ssa/src/mir/place.rs) in MIR in the following way:
Choosing a target-specific name (bpf_*/btf_*) would mean poisoning this logic with target-specific concepts. It would also require add an another boolean if an another relocation mechanism (e.g. Swift ABI) is added.
Keeping the name neutral keeps MIR target-agnostic and adds only one boolean. Therefore, I would rather call it "relocatable".
Great suggestion. We could add the following Rust intrinsics, which would lower to the following LLVM intrinsics.
With neutral naming, this could eventually be implemented for other relocation mechanisms (e.g. Swift ABI):
Does it have to be a feature of structs and arrays?
Could it instead be expressed as a macro or intrinsic function that gives an offset to use with ptr::read? (which could be sufficient to make getters and setters for something struct-like that isn't an actual struct)
Or maybe it could be implemented as getters and setters on extern opaque types?
It wouldn't be as neat as 1st class structs that work with literals and pattern matching, but might be sufficient to interact with kernel structs treating them as an API.
I think the current implementation in LLVM makes it quite hard to implement with anything else than real structs:
Using @llvm.preserve.*.access.index intrinsics instead of getelementptr in LLVM has two effects:
It ensures the offset stays literally in the code and is not transformed by optimization passes,
and it allows debuginfo metadata with type information to be attached to the access.
The BPF codegen backend then writes the used type names and field names into a symbol table and generates BTF relocations that connect each such offset in the code with the symbolic names of the accessed type and field and type layout information.
With a macro or intrinsic function, the compiler would still need to generate type debug metadata, so that the BPF codegen backend can find the names and check the types for compatibility. It's probably way easier to reuse the infrastructure that already generates debuginfo metadata for Rust types than to reimplement part of it just for BPF.
re: "relocatable". I would definitely not call it "bpf_relocatable" (target specific), but between "relocatable" and "btf_relocatable", BTF is the means through which relocation information is captured (BTF types/fields) and expressed (.BTF.ext section), so btf_ prefix seems appropriate. For this Swift use case you were talking about, would relocation information be expressed differently somehow?
In the end it doesn't matter all that much, but too-generic "relocatable" has a potential to be, well, too generic and thus confusing. But just my 2 cents.
re: instrinsics, make sense and seems like a good idea. will the "does it exist" primitive be expressed as an extra argument to "preserve_xxx_info" or it would require its own set of intrinsics?
Sorry for the silence. I took some time to re-think the entire approach and at the end, together with tamird, we came up with way simpler solution that will not require any changes to the field projection, and aligns with what @kornel proposes.
Yes, after experimenting with that idea, I'm convinced this might be a good direction.
For reference, clang has the following intrinsic that allows to request information about BTF-relocatable type:
And... that would be it on Rust compiler's side. No changes to MIR and the field projection. No direct exposure of preserve_access LLVM intrinsics (at least to the users).
Why?
With BTF-relocatable types, we are not sure whether a field exists during a runtime. Therefore, a field access does not provide any guarantees of soundess. That goes against Rust's guarantees of safety.
What goes in line with Rust's safety guarantees, however, is a way to access a relocatable field only if we meet certain circumstances - that the field exists, and that the size of the field is as we expect.
The mentioned intrinsics would provide building blocks for safe abstraction for accessing relocatable fields. To be precise - such safe abstraction could be provided outside of Rust standard library, it could be done in Aya or a dedicated third-party library. The first step towards safety would be the following function with an error type:
#[derive(Debug, Eq, PartialEq)]
pub enum RelocatableFieldAccessError {
SizeMismatch { expected: usize, actual: usize },
ProbeRead(i32),
}
#[inline(always)]
pub fn relocatable_field_read<T>(ptr: *const T) -> Result<Option<T>, RelocatableFieldAccessError> {
if relocatable_field_exists(ptr) {
let expected = core::mem::size_of::<T>();
let actual = relocatable_field_byte_size(ptr);
if expected == actual {
let offset = relocatable_field_byte_offset(ptr);
// SAFETY: We trust LLVM to emit a correct BTF relocation for
// that offset, and we trust BPF loader libraries to patch it
// accordingly.
let field = unsafe {
bpf_probe_read_kernel(&*ptr.add(offset))
.map_err(|code| RelocatableFieldAccessError::ProbeRead(code))?
};
Ok(Some(field))
} else {
Err(RelocatableFieldAccessError::SizeMismatch { expected, actual })
}
} else {
Ok(None)
}
}
And then a BTF-relocatable structure with getters that use the function:
Generation of such getters could be hidden behind a proc macro.
That would also solve the problem of different kernel versions removing or renaming fields, without having to rely on version suffixes mechanism from clang that was described by @anakryiko and mentioned by @Jules-Bertholet.
The only point from @kornel's proposition which I'm unsure about is using extern opaque types. We would need some way of declaring its layout and letting Rust (and a backend, like LLVM) to generate the debug info for it. The weakness of the code example I provided above is that the task_struct type is a sized struct. An open question is how can it be marked as !Sized (or const Sized), perhaps that brings us back to the #[repr(btf)] idea.
After giving it more thought, I think that btf_ prefix definitely makes sense for intrinsics. I don't expect intrinsics to be shared between different types of field relocations (e.g. BTF, Swift).
So the intrinsics I proposed above could be called:
Field projection is where a more unified design could make more sense though.
I take this point back - BPF verifier will always reject programs with field accesses to non-existing fields, so the field projection is still safe. That said, I think it makes sense to start only with intrinsics now, and leave the field projection topic for future.
Given that there are no objections to the feature itself, I went ahead and submitted an RFC, trimmed down only to intrinsics, on GitHub:
I've made a small change to the RFC since yesterday - I have realized that intrinsics taking field: usize aren't a good design for nested field access, and would require multiple subsequent calls, therefore lowering to more LLVM intrinsic calls than necessary. It also requires the caller to keep track of field indices, which seems unnecessary, given that the compiler has the information about the type layout already.
What works better in that case are builtin-syntax wrapper macros, like: