spinlock: Add initial implementation

Change-Id: I687d54b15f47db651d3d0ce0475846110f8343c0
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/maize/+/256913
Pigweed-Auto-Submit: Erik Gilling <konkers@google.com>
Commit-Queue: Auto-Submit <auto-submit@pigweed-service-accounts.iam.gserviceaccount.com>
Presubmit-Verified: CQ Bot Account <pigweed-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Travis Geiselbrecht <travisg@google.com>
Lint: Lint 🤖 <android-build-ayeaye@system.gserviceaccount.com>
diff --git a/arch/arm_cortex_m/BUILD.bazel b/arch/arm_cortex_m/BUILD.bazel
new file mode 100644
index 0000000..e14d276
--- /dev/null
+++ b/arch/arm_cortex_m/BUILD.bazel
@@ -0,0 +1,27 @@
+# Copyright 2025 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load("@rules_rust//rust:defs.bzl", "rust_library")
+
+package(default_visibility = ["//visibility:public"])
+
+rust_library(
+    name = "spinlock_backend_cortex_m",
+    srcs = ["spinlock_backend_cortex_m.rs"],
+    crate_name = "spinlock_backend",
+    deps = [
+        "//kernel/sync:spinlock_core",
+        "@rust_crates//:cortex-m",
+    ],
+)
diff --git a/arch/arm_cortex_m/spinlock_backend_cortex_m.rs b/arch/arm_cortex_m/spinlock_backend_cortex_m.rs
new file mode 100644
index 0000000..5c88aa6
--- /dev/null
+++ b/arch/arm_cortex_m/spinlock_backend_cortex_m.rs
@@ -0,0 +1,115 @@
+// Copyright 2025 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#![no_std]
+
+use core::cell::UnsafeCell;
+use core::mem::ManuallyDrop;
+
+use cortex_m::interrupt;
+use cortex_m::register::primask::{self, Primask};
+use spinlock_core::BareSpinLockApi;
+
+struct InterruptGuard {
+    saved_primask: Primask,
+}
+
+impl InterruptGuard {
+    fn new() -> Self {
+        let saved_primask = primask::read();
+
+        // `cortex_m::interrupt::disable()` handles proper fencing.
+        interrupt::disable();
+
+        Self { saved_primask }
+    }
+}
+
+impl Drop for InterruptGuard {
+    #[inline]
+    fn drop(&mut self) {
+        if self.saved_primask.is_active() {
+            // `cortex_m::interrupt::enable()` handles proper fencing.
+            //
+            // Safety: cortex-m critical sections are not used in this code base.
+            unsafe { interrupt::enable() };
+        }
+    }
+}
+
+pub struct CortexMSpinLockGuard<'a> {
+    // A size optimization that allow the sentinel's drop() to directly
+    // call `InterruptGuard::do_drop()`.
+    guard: ManuallyDrop<InterruptGuard>,
+    lock: &'a BareSpinLock,
+}
+
+impl Drop for CortexMSpinLockGuard<'_> {
+    #[inline]
+    fn drop(&mut self) {
+        unsafe {
+            self.lock.unlock();
+            ManuallyDrop::drop(&mut self.guard);
+        };
+    }
+}
+
+/// Non-SMP bare spinlock
+pub struct BareSpinLock {
+    // Lock state is needed to support `try_lock()` semantics.  An `UnsafeCell`
+    // is used to hold the lock state as exclusive access is guaranteed by
+    // enabling and disabling interrupts.
+    is_locked: UnsafeCell<bool>,
+}
+
+impl BareSpinLock {
+    pub const fn new() -> Self {
+        Self {
+            is_locked: UnsafeCell::new(false),
+        }
+    }
+
+    // Must be called with interrupts disabled.
+    #[inline]
+    unsafe fn unlock(&self) {
+        *self.is_locked.get() = false;
+    }
+}
+
+impl Default for BareSpinLock {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl BareSpinLockApi for BareSpinLock {
+    type Guard<'a> = CortexMSpinLockGuard<'a>;
+
+    fn try_lock(&self) -> Option<Self::Guard<'_>> {
+        let guard = InterruptGuard::new();
+        // Safety: exclusive access to `is_locked` guaranteed because interrupts
+        // are off.
+        if unsafe { *self.is_locked.get() } {
+            return None;
+        }
+
+        unsafe {
+            *self.is_locked.get() = true;
+        }
+
+        Some(CortexMSpinLockGuard {
+            guard: ManuallyDrop::new(guard),
+            lock: self,
+        })
+    }
+}
diff --git a/kernel/sync/BUILD.bazel b/kernel/sync/BUILD.bazel
new file mode 100644
index 0000000..f07f0a1
--- /dev/null
+++ b/kernel/sync/BUILD.bazel
@@ -0,0 +1,56 @@
+# Copyright 2025 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load("@pigweed//pw_build:compatibility.bzl", "incompatible_with_mcu")
+load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test")
+
+package(default_visibility = ["//visibility:public"])
+
+rust_library(
+    name = "spinlock",
+    srcs = ["spinlock.rs"],
+    deps = [
+        ":spinlock_backend",
+        ":spinlock_core",
+    ],
+)
+
+rust_test(
+    name = "spinlock_test",
+    crate = ":spinlock",
+    use_libtest_harness = False,
+    visibility = ["//visibility:public"],
+    deps = [
+        "//lib/unittest",
+        "//target:linker_script",
+    ],
+)
+
+rust_library(
+    name = "spinlock_core",
+    srcs = ["spinlock_core.rs"],
+)
+
+rust_library(
+    name = "spinlock_backend_atomic",
+    srcs = ["spinlock_backend_atomic.rs"],
+    crate_name = "spinlock_backend",
+    target_compatible_with = incompatible_with_mcu(),
+    deps = [":spinlock_core"],
+)
+
+label_flag(
+    name = "spinlock_backend",
+    build_setting_default = ":spinlock_backend_atomic",
+)
diff --git a/kernel/sync/spinlock.rs b/kernel/sync/spinlock.rs
new file mode 100644
index 0000000..7e30ae9
--- /dev/null
+++ b/kernel/sync/spinlock.rs
@@ -0,0 +1,106 @@
+// Copyright 2025 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#![no_std]
+#![cfg_attr(test, no_main)]
+
+use core::cell::UnsafeCell;
+use core::ops::{Deref, DerefMut};
+
+pub use spinlock_backend::BareSpinLock;
+pub use spinlock_core::*;
+
+pub struct SpinLockGuard<'lock, T> {
+    lock: &'lock SpinLock<T>,
+    _inner_guard: <BareSpinLock as BareSpinLockApi>::Guard<'lock>,
+}
+
+impl<T> Deref for SpinLockGuard<'_, T> {
+    type Target = T;
+
+    fn deref(&self) -> &T {
+        unsafe { &*self.lock.data.get() }
+    }
+}
+
+impl<T> DerefMut for SpinLockGuard<'_, T> {
+    fn deref_mut(&mut self) -> &mut T {
+        unsafe { &mut *self.lock.data.get() }
+    }
+}
+
+pub struct SpinLock<T> {
+    data: UnsafeCell<T>,
+    inner: BareSpinLock,
+}
+
+impl<T> SpinLock<T> {
+    pub fn new(initial_value: T) -> Self {
+        Self {
+            data: UnsafeCell::new(initial_value),
+            inner: BareSpinLock::new(),
+        }
+    }
+
+    pub fn try_lock(&self) -> Option<SpinLockGuard<'_, T>> {
+        self.inner.try_lock().map(|guard| SpinLockGuard {
+            lock: self,
+            _inner_guard: guard,
+        })
+    }
+
+    pub fn lock(&self) -> SpinLockGuard<'_, T> {
+        let inner_guard = self.inner.lock();
+        SpinLockGuard {
+            lock: self,
+            _inner_guard: inner_guard,
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use unittest::test;
+
+    #[test]
+    fn bare_try_lock_returns_correct_value() -> unittest::Result<()> {
+        let lock = BareSpinLock::new();
+
+        {
+            let _sentinel = lock.lock();
+            unittest::assert_true!(lock.try_lock().is_none());
+        }
+
+        unittest::assert_true!(lock.try_lock().is_some());
+
+        Ok(())
+    }
+
+    #[test]
+    fn try_lock_returns_correct_value() -> unittest::Result<()> {
+        let lock = SpinLock::new(false);
+
+        {
+            let mut guard = lock.lock();
+            *guard = true;
+            unittest::assert_true!(lock.try_lock().is_none());
+        }
+
+        let guard = lock.lock();
+        unittest::assert_true!(*guard);
+
+        Ok(())
+    }
+}
diff --git a/kernel/sync/spinlock_backend_atomic.rs b/kernel/sync/spinlock_backend_atomic.rs
new file mode 100644
index 0000000..c6c2862
--- /dev/null
+++ b/kernel/sync/spinlock_backend_atomic.rs
@@ -0,0 +1,62 @@
+// Copyright 2025 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#![no_std]
+
+use core::sync::atomic::{AtomicBool, Ordering};
+
+use spinlock_core::BareSpinLockApi;
+
+pub struct AtomicSpinLockGuard<'a> {
+    lock: &'a BareSpinLock,
+}
+
+impl Drop for AtomicSpinLockGuard<'_> {
+    fn drop(&mut self) {
+        self.lock.unlock();
+    }
+}
+
+pub struct BareSpinLock {
+    locked: AtomicBool,
+}
+
+impl BareSpinLock {
+    pub const fn new() -> Self {
+        Self {
+            locked: AtomicBool::new(false),
+        }
+    }
+
+    // Only to be called by AtomicSpinlockSentinel::drop().
+    fn unlock(&self) {
+        self.locked.store(false, Ordering::Release);
+    }
+}
+
+impl Default for BareSpinLock {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl BareSpinLockApi for BareSpinLock {
+    type Guard<'a> = AtomicSpinLockGuard<'a>;
+
+    fn try_lock(&self) -> Option<Self::Guard<'_>> {
+        self.locked
+            .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
+            .map(|_| AtomicSpinLockGuard { lock: self })
+            .ok()
+    }
+}
diff --git a/kernel/sync/spinlock_core.rs b/kernel/sync/spinlock_core.rs
new file mode 100644
index 0000000..941a831
--- /dev/null
+++ b/kernel/sync/spinlock_core.rs
@@ -0,0 +1,38 @@
+// Copyright 2025 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#![no_std]
+
+pub trait BareSpinLockApi {
+    type Guard<'a>
+    where
+        Self: 'a;
+
+    // Rust does not support const function in traits.  However it should be
+    // considered that the BareSpinlockApi includes:
+    //
+    // `const fn new() -> Self`
+
+    fn try_lock(&self) -> Option<Self::Guard<'_>>;
+
+    fn lock(&self) -> Self::Guard<'_> {
+        loop {
+            if let Some(sentinel) = self.try_lock() {
+                return sentinel;
+            }
+        }
+    }
+
+    // TODO - konkers: Add optimized path for functions that know they are in
+    // atomic context (i.e. interrupt handlers).
+}
diff --git a/lib/unittest/BUILD.bazel b/lib/unittest/BUILD.bazel
index 4183b61..3f15abf 100644
--- a/lib/unittest/BUILD.bazel
+++ b/lib/unittest/BUILD.bazel
@@ -12,9 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
-load("@pigweed//pw_build:pw_linker_script.bzl", "pw_linker_script")
-load("@rules_rust//rust:defs.bzl", "rust_library", "rust_proc_macro", "rust_test")
 load("@pigweed//pw_build:compatibility.bzl", "incompatible_with_mcu")
+load("@rules_rust//rust:defs.bzl", "rust_library", "rust_proc_macro", "rust_test")
 
 package(default_visibility = ["//visibility:public"])
 
diff --git a/target/qemu/BUILD.bazel b/target/qemu/BUILD.bazel
index f44b533..ec2a5dc 100644
--- a/target/qemu/BUILD.bazel
+++ b/target/qemu/BUILD.bazel
@@ -26,8 +26,9 @@
         "@rust_crates//:no_std",
     ],
     flags = flags_from_dict({
-        "//target:linker_script": "//target/qemu/linker_scripts:qemu_lm3s6965_linker_script",
         "//lib/unittest:unittest_runner": "//lib/unittest:unittest_runner_cortex_m",
+        "//kernel/sync:spinlock_backend": "//arch/arm_cortex_m:spinlock_backend_cortex_m",
+        "//target:linker_script": "//target/qemu/linker_scripts:qemu_lm3s6965_linker_script",
         "@pigweed//pw_log/rust:pw_log_backend": "//target/qemu/pw_log_backend_qemu:pw_log_backend",
     }),
 )
@@ -42,6 +43,7 @@
         "@rust_crates//:no_std",
     ],
     flags = flags_from_dict({
+        "//kernel/sync:spinlock_backend": "//arch/arm_cortex_m:spinlock_backend_cortex_m",
         "//target:linker_script": "//target/qemu/linker_scripts:qemu_nrf51823_linker_script",
         "@pigweed//pw_log/rust:pw_log_backend": "//target/qemu/pw_log_backend_qemu:pw_log_backend",
     }),