-
Notifications
You must be signed in to change notification settings - Fork 141
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow for cloning or owning of major data types #448
Comments
I don't think it's good to really transfer ownership of map or prog out of bpf_object itself, because that goes strictly against ownership model in libbpf. But taking #441 into account and this issue, I think it might make sense to have some sort of lightweight "map view/handle" (and similarly for program) that would be disassociated from Object itself and would contain FD and some minimal information internally to allow to
If both these handle structs and real Object's map/prog could implement the same trait for the above operations, you could use both real map/progs and such handles interchangeably for some generic code. Similarly for #441, instead of pretending to reconstruct full-fledged OpenMap and OpenProgram, would it be safer (from future API evolution) to only allow to reconstruct map/prog handle from ID, not OpenMap/OpenProgram? @danielocfb thoughts? |
Almost no operations on a |
It honestly seems over engineered to me. If the proposed lightweight objects don't carry around a reference to the "original" If the above suggestion of using If references themselves are the problem, then perhaps we can think about storing I suspect |
In my mental model I had of Map, I had mistakenly assumed that By only taking immutable references, I can indeed have multiple maps floating around at the same time. That's something I hadn't considered before. This does seem to solve the issue of exclusivity, at least for the use case I'm looking for. I will have to look at this more closely tomorrow though, so far I only had a quick look. As for the references, I'm concerned that I have to assure the correct lifetimes of the map references. I will have to check whether I can live with that. My main concern here is that if I want to fully modularize a program where each module is managing/using a map or two, it can get difficult to track the lifetime of the references. I've walked down a similar path before and ended up with named lifetimes everywhere, which got more painful the more modules were involved. You're basically forced into a pattern where you have to use a central dispatcher routine - which holds the main Object - and all references are created from within that dispatcher routine. If your modules now throw async futures or sometimes just closures into the mix, you quickly overwhelm the capabilities of the borrow checker.
I personally found the idea interesting. The extra error path is certainly a bit annoying, but that's the price to pay if the borrow checker isn't capable of tracking the reference lifetimes by itself. As for the traits, I'm mostly thinking about a handle for Map and probably for Program, so it doesn't seem to convoluted for me at first glance. |
We discussed this offline with @danielocfb. My main concern was around possibility to construct Map using above referenced For the original issue here, I agree that non-mutable reference to Object's Map should help. |
Yes, I agree, it can be a problem. I'll let you check first whether it's one for you, though.
I think there certainly could be some merit, but right now I think of such handles as a last resort. A cleaner solution to me, would be to hand out |
Okay, so I just discussed this option again with @anakryiko . And basically the semantics would be different than what I expected them to be: the handle would hold a reference to the map and the corresponding object could be closed, while the handle would still keep working. |
I had a closer look again at this today and tried out a few things. Having all maps as immutable definitely helps, even though carrying named lifetimes around everywhere is all but pretty. Definite problems appeared once futures were involved. There are two primary things that make this impossible:
I'm not sure I fully understand the proposal in that case. The handles would themselves be owned objects that point to a map (via FD?) and ensure that the map is kept alive, i.e. the map can outlive the object? |
I don't know if that's the definite conclusion to draw form this discussion, but we should most certainly look into whether
As I understand it, the handle would mostly be a file descriptor (fd). But because the fd is dup'ed the kernel keeps the corresponding object (the map) alive until all references are released. That's the case even if the BPF object is released. So it's not about language properties and reference counting at that level, but more of a system guarantee if you will. |
Okay, so perhaps we should consider a rework of the map APIs altogether. In the larger scheme of things, we already have some implementation details that are not particularly clean. Specifically, we capture both the libbpf-rs/libbpf-rs/src/map.rs Lines 213 to 214 in 208d02e
That was done in response to #191. The initially proposed I feel like we should capture all the use cases that we need to support and then we can come up with an API design. We should also figure out what bits of information we can provide on which creation path. E.g., @anakryiko mentioned that we would not necessarily have BTF information (among other details) when creating from, say, an ID. So that's relevant for the libbpf-rs/libbpf-rs/src/map.rs Lines 275 to 276 in 208d02e
as part of 921de03. We should decide whether from the libbpf-rs side of things, the lack of such details matters much and justifies a dedicated type, or whether it makes more sense to just expose them as optional ( |
I had a look into this, even though I'm not sure there's any benefit to it when you can't actually own a map: Moving a reference (between threads) requires both Send and Sync (according to the Rust book, a reference that is It also appears that Implementing
Looking at the libbpf internals, for threading/futures it would probably be a lot easier if we did not have a |
@GermanCoding this is not strictly true, there is nothing about inherently not thread safe abour raw pointers, they are marked as !Send + !Sync more as a lint. So removing the raw pointer and using just the fd is equivalent in terms of safety to doing On that note though, I've actually done the work of looking at every C (libbpf) method that is called inside every method in |
@GermanCoding On the topic of Map not being Send and Sync, it is cumbersome but you can work around it with |
I know, but The Rustonomicon states:
In this case, the ptr references a There's a special case here, because we cannot actually own Maps, and references are protected anyway. But that's another topic actually, because
No, because by removing the |
You are correct, but there is something else that can stop
The fact that it shares state with |
So I found some time to have a look at this again today.
I also noticed this. The current API internally distinguishes "map created from userspace - no bpf_map pointer" and "map created by libbpf - have bpf_map pointer". This creates some weird codepaths that are absolutely not obvious from a user perspective. For example, it appears that I can pin
However, if the user then proceeds with calling
Same for
So I can pin a userspace-created map, but then trying to query information about it won't work. The error handling also feels a bit inconsistent, sometimes returning Results and sometimes Options. This is obviously an effect that comes from the fact that we don't have a For me, this sounds like this difference is severe enough to warrant distinct types for libbpf-created and "other" maps. This way we can explicitly signal what things are supported for the given Map and what are not. My own use case also comes into play here: The "other" map type (those without a |
Indeed, that is not particularly great behavior. Are you interested in prototyping "handle" suggestion, @GermanCoding? I feel like having something concrete to talk about and iterate on may be best to move things forward. |
Yes, I would love to do that. I will need a few days time though, as I can only work on this part time. I would then open a PR and we continue discussing there? |
Yes, that would be great. No rush! |
This issue should now have been addressed with #473 merged. Closing. |
Hello,
This is more of an API design question, though I couldn't find any contact information for a discussion. Thus I'm writing this as an issue.
I'm currently evaluating libbpf-rs for production usage. I've previously looked at Aya, but it's lacking BTF support - which is increasingly becoming more and more important.
While trying out a prototype conversion from Aya to libbpf-rs, the most pain I have is that libbpf-rs uses a lot of references. For many structs, it appears impossible to obtain ownership of them.
For example, if I want to work with a map, I can call
libbpf_rs::Object::map_mut()
, which gives me a mutable reference to that map. I can then add, remove or lookup elements. This is fine for small projects, i.e. if all of my program logic is inmain()
.However, on a larger codebase, I'm working with a number of maps and I possibly want references to multiple maps at the same time. Due to Rusts borrow checker, this is impossible with libbpf-rs: I cannot call
map_mut()
twice (without dropping the map reference first), since that would require two mutable borrows of the Object.This makes working with maps really painful, as I'm pretty much forced to pass around an
Arc<Mutex<Object>>
-style object to have multiple ownership of the Object and then get temporary references to the maps and drop them immediately afterwards.I found a small hack to circumvent this: I can effectively clone a map by doing something like this:
libbpf_rs::Map::from_map_id(object.info()?.info.id)?
(i.e. creating a new Map from an existing map ID). This feels like a hack around the underlying problem though - if this were intended API usage, Map should implementClone
by itself. The omission of Clone on pretty much everything feels like it was intentional. But I'm having trouble on designing a clean API around libbpf-rs, since it's heavy usage of references makes it difficult to juggle multiple programs, maps and stuff around.I've seen that libbpf-rs currently has this statement in its code for the Bpf object:
This makes complete sense. By only allowing references we can ensure that i.e. maps do not outlive the BPF object. However, it also disallows a lot of API designs and removes flexibility.
Aya for example also uses a similar approach by default. However, Aya has functions like
take_map
that return owned maps. Logically this transfers ownership from the Bpf Object to the caller, who is now responsible for managing the map's lifetime: Once the map struct is dropped, the map is gone forever. This allows for complex programs, where different modules can be in charge of a given map, instead of centralizing everything around a single Bpf Object.The text was updated successfully, but these errors were encountered: