KMutex: Technical Design Document

This document covers the technical architecture, safety invariants, and macro code generation details of the ksync crate.

1. Technical Architecture

ksync implements a token-based synchronization pattern (also known as the “Ghost Token” pattern). Instead of encapsulating the protected data inside the mutex struct itself (like std::sync::Mutex<T>), it separates the lock state (KMutex) from the actual data storage (KCell).

+-------------------------------------------------------------+
|                        Parent Struct                        |
|                                                             |
|   +----------------+           +------------------------+   |
|   |  KMutex<Class> |           |  KCell<T, Class>       |   |
|   |  (Raw Lock)    |           |  (UnsafeCell wrapper)  |   |
|   +--------+-------+           +-----------+------------+   |
+------------|-------------------------------|----------------+
             | lock()                        | get(token)
             v                               v
    +--------+------------+         +--------+-------+
    |  KMutexGuard        |         |  &T / &mut T   |
    |                     |-------->|  (Safe Access) |
    |  - LockToken        |         +----------------+
    +---------------------+

The Three Core Types

  1. LockToken<'a, Class>: A zero-sized type (ZST) that serves as compile-time proof that the exclusive lock for Class is currently held by the current thread. It has a lifetime 'a bound to the active KMutexGuard.
  2. KMutex<Class>: The lock state representation. It wraps a raw mutex (e.g., fuchsia_sync::RawMutex). Locking it acquires the raw lock and returns a KMutexGuard holding the ZST LockToken.
  3. KCell<T, Class>: A wrapper around core::cell::UnsafeCell<T>. It provides token-gated accessors (get(&self, token) and get_mut(&self, token)).

Guard Types & The Instance-Bound Soundness Model

While the LockToken and KCell share a compile-time Class type parameter to prevent mixing locks of different types, this type-level association is insufficient to guarantee memory safety.

To provide safety, the macro generates Guard structures (MyStructMuGuard) that provide a safe wrapper that provides the instance-level association. The Guard holds a reference to the specific parent struct instance (parent: &'a MyStruct) and the active lock state (inner: KMutexGuard<'a, MyStructMuClass>), providing the link needed to make accessing each KCell safe.

2. Safe Exclusive Access

If a thread has unique exclusive access to the KCell container itself (either by holding ownership self or an exclusive borrow &mut self), it doesn't need a runtime lock token to access the data safely. The Rust borrow checker already guarantees compile-time thread exclusivity:

  • get_inner_mut(&mut self) -> &mut T: Accesses the inner value mutably via UnsafeCell::get_mut(). This is safe because the &mut self borrow ensures no other borrows of the cell are active.
  • into_inner(self) -> T: Consumes the KCell container and returns the inner value T safely via UnsafeCell::into_inner().

3. Macro Code Generation

The #[guarded] attribute proc-macro parses a struct definition, rewrites its fields, and generates the accompanying safe Guard and Split Accessor structures.

3.1 Input Struct

#[guarded]
pub struct MyStruct {
    #[mutex]
    pub mu: KMutex,

    #[guarded_by(mu)]
    pub data1: u32,

    #[guarded_by(mu)]
    data2: i32,
}

3.2 Expanded Code Output (Simplified)

// 1. Unique Lock Class marker struct generated automatically
pub struct MyStructMuClass;

// 2. Struct fields rewritten to KMutex<Class> and KCell<T, Class>
pub struct MyStruct {
    pub mu: ::ksync::KMutex<MyStructMuClass>,
    pub data1: ::ksync::KCell<u32, MyStructMuClass>,
    data2: ::ksync::KCell<i32, MyStructMuClass>,
}

// 3. Custom Guard generated with safe target accessors and lifetime bindings
pub struct MyStructMuGuard<'a> {
    parent: &'a MyStruct,
    inner: ::ksync::KMutexGuard<'a, MyStructMuClass>,
}

impl<'a> MyStructMuGuard<'a> {
    // Individual Accessors (matching original field visibilities)
    #[inline]
    pub fn data1(&self) -> &u32 {
        // SAFETY: The token is obtained from the same parent instance (self.parent)
        // that contains the cell, satisfying the KCell safety invariant.
        unsafe { self.parent.data1.get(self.inner.token()) }
    }

    #[inline]
    pub fn data1_mut(&mut self) -> &mut u32 {
        // SAFETY: The token is obtained from the same parent instance (self.parent)
        // that contains the cell, satisfying the KCell safety invariant.
        unsafe { self.parent.data1.get_mut(self.inner.token_mut()) }
    }

    #[inline]
    fn data2(&self) -> &i32 {
        // SAFETY: The token is obtained from the same parent instance (self.parent)
        // that contains the cell, satisfying the KCell safety invariant.
        unsafe { self.parent.data2.get(self.inner.token()) }
    }

    #[inline]
    fn data2_mut(&mut self) -> &mut i32 {
        // SAFETY: The token is obtained from the same parent instance (self.parent)
        // that contains the cell, satisfying the KCell safety invariant.
        unsafe { self.parent.data2.get_mut(self.inner.token_mut()) }
    }

    #[inline]
    pub fn fields<'b>(&'b self) -> MyStructMuFields<'b> {
        MyStructMuFields {
            // SAFETY: The token is from the same parent instance as the cell.
            data1: unsafe { self.parent.data1.get(self.inner.token()) },
            data2: unsafe { self.parent.data2.get(self.inner.token()) },
            _marker: ::core::marker::PhantomData,
        }
    }

    #[inline]
    pub fn fields_mut<'b>(&'b mut self) -> MyStructMuFieldsMut<'b> {
        let token = self.inner.token_mut();
        // SAFETY:
        // 1. We have exclusive access to the Guard (&mut self).
        // 2. The fields in the struct are disjoint.
        // 3. The returned references are bound to the lifetime 'b of the guard borrow,
        //    preventing them from outliving the guard.
        unsafe {
            MyStructMuFieldsMut {
                data1: &mut *self.parent.data1.as_mut_ptr(token),
                data2: &mut *self.parent.data2.as_mut_ptr(token),
                _marker: ::core::marker::PhantomData,
            }
        }
    }
}

// 4. Helper split structs generated to hold the disjoint borrows
pub struct MyStructMuFields<'b> {
    pub data1: &'b u32,
    data2: &'b i32,
    _marker: ::core::marker::PhantomData<(&'b (), )>,
}

pub struct MyStructMuFieldsMut<'b> {
    pub data1: &'b mut u32,
    data2: &'b mut i32,
    _marker: ::core::marker::PhantomData<(&'b (), )>,
}

// 5. Lock method impl on the parent struct
impl MyStruct {
    #[inline]
    pub fn lock_mu(&self) -> MyStructMuGuard<'_> {
        MyStructMuGuard {
            parent: self,
            inner: self.mu.lock(),
        }
    }
}