Skip to content

LockIniter API concerns #772

Closed
Closed
@y86-dev

Description

@y86-dev

Dear Rust-for-Linux team,

I want to raise some important concerns regarding some of the API design of this project.
If this is the wrong way to communicate this concern, then please direct me to right way of raising my .

My background

I am mainly a Rust developer and only have limited experience with the Linux code base. I have programmed in Rust extensively, so I probably have a rust-centric perspective on the project and might not know the constraints you are working under.

My view on this project

Let me first state this: I love this project and only wish it the best!
I personally use Linux and love the thought of integrating the safety of rust in the most often written parts of the code.

Yet, I am deeply frustrated with some of the choices made during API design. I feel like if these were pushed into mainline Linux code base hastily, it would become a legacy nightmare in the future, because you cannot remove a unsafe, already deprecated API, because a lot of existing code will depend on it. I want to avoid this future:

use kernel::sync2::Mutex;

The LockIniter API

I am singling out the LockIniter API, because that is all I managed to look at in the short time span. Locks are one of the most important primitives, so if their API has poor design that will have severe implications for all future code in the kernel.

I think it is a fatal flaw that you are required to use unsafe when you want to create and initialize a lock. This issue has been previously raised in a more general form (#290). I want to shortly illustrate the issue using a Mutex as an example (but other locks have the same problem, because they need to be pinned to be used), go over the suggested solution and its problems from the previous thread and my proposal to fix this.

(The following code snippets have been copied from samples/rust/rust_sync.rs)

In the init function of the kernel module, a Mutex is created inside of a Box and then initialized.

// SAFETY: `init` is called below
let mut data = Pin::from(Box::try_new(unsafe { Mutex::new(0) })?);
mutex_init!(data.as_mut(), "RustSync::init::data1");

After this point no more unsafe is needed to use the mutex, which is good. However in order to call Mutex::new, we need to uphold the invariant that we call Mutex::init_lock before we use it. Because locks are a fundamental tool that will be used in a majority of modules, this creates many unsafe blocks that could be avoided with better API design.

This piece of code is in my eyes not really an improvement over the C code. Yes, there is a safety comment and a clear identification of an unsafe operation. But this has two problems: It annoys C programmers, because while they still program using their old ways, they now need to occasionally sprinkle in an unsafe block, which does not really seem beneficial. Rust devs are annoyed, because there exist nicer abstractions that could solve this in a way more idiomatic way.
When we integrate Rust into the Linux codebase, we should use all advantages that the language can give us, like compiler-checked invariants and memory safety. But when we use unsafe in major places like this, we basically end up with C-style code in the Rust syntax.

Solution 1: Extra Allocations

Just store the lock in Pin<Box<_>> (or other smart pointers) and only let the lock hand out those instances.
But the additional allocation and indirection are obviously inefficient.

Solution 2: nbdd0121's Approach

While I have not extensively looked at their approach, I can say a few things:

  • It achieves safe pinned initialization using a single function call
  • It achieves safe wrapping of multiple types that need pinned init
  • It uses closures, which seems weird and a little bit verbose to me
  • There is no way to store an unpinned mutex that you want to initialize later
  • One needs to use a macro to compose objects

While it seems like this approach works as intended, the discussion died down, probably because of the proc-macro integration issue.

Solution 3: My personal approach

In the short time that I had since I came across this project, I only partially managed to implemented my own solution. It is similar to nbdd0121's approach, as it also uses a proc-macro. See below for the discussion.

My approach centers around using transmutation of types to allow a separation of the creation of MutexUninit<T> and its subsequent initialization to a Mutex<T>, such that one is only able to initialize if they have access to Pin<P> where P: DerefMut<MutexUninit<T>>.

This allows the call site to look like this:

let data = Pin::from(Box::try_new(MutexUninit::new(0))?);
let mut data = data.init();

It leverages the type system to ensure that one cannot call lock on MutexUninit. This way, forgetting to initialize the lock or attempting to initialize it twice results in a compilation error.
Now, unsafe is only needed to create bottom level primitives that need to call unsafe functions themselves. At the declaration site (e.g. the cases where you want to create a struct containing multiple mutexes) you can simply use a proc-macro on the struct and mark the types that require initialization.

I created a crate as a proof-of-concept. If you want to take a look at it, I published it here. I am currently still working to improve it.

The main problem with this approach may be the usage of a proc-macro, similar to nbdd0121's solution. I'm a little late for the discussion, but I still have a few additional points to add:

  • syn is very dependable, big parts of the rust ecosystem (including the rustc itself!) depend on it. There should be no problem using it, as bindgen is already integrated.
  • proc-macros are the idiomatic way in Rust to implement features that are not part of the language. When a new feature for the language is proposed, one of the many questions that comes up is "Could this also be done with a proc-macro?" If the answer is yes, then it becomes increasingly unlikely that the feature is added as a language construct.
  • Just like the C ecosystem, the rust ecosystem also adds its tools. Proc macros are an invaluable tool for any rust programmer and very much part of the language, just like declarative macros, functions etc.
  • If the unsafe code is largely contained within some select few proc macros, then that code can be checked carefully. If an error is found, it does not need to be fixed at every invocation site. This might not seem too important in this specific case, but always following this design guideline will pay off in the long term.

Obviously, I do not think that proc-macros are the right tool for every problem, but this is definitely an area where a proc macro is the preferred and idiomatic way.

Summary

I apologize for the late interruption in your progress. I am very much aware that you probably cannot wait to integrate your progress into the mainline kernel and finally add support for Rust in Linux. And believe me, I am as excited as you are and I am very thankful for your work. I just wanted to raise my concerns over the API design and I hope you can understand why I am writing you here.

Metadata

Metadata

Assignees

No one assigned

    Labels

    • libRelated to the `rust/` library.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions