What’s new in Pigweed: March 2026#

Highlights:

Async and concurrency#

Handle futures of varying types with BoxedFuture#

pw::async2::BoxedFuture makes it easier to store or return futures of different concrete types. It type-erases futures and stores them dynamically via pw::Allocator, hiding the concrete type. Example:

#include "pw_async2/box.h"

pw::async2::BoxedFuture<MyData> GetData(pw::Allocator& alloc, bool use_cache) {
  if (use_cache) {
    // Return a future of one type in this scenario…
    return pw::async2::BoxFuture(alloc, cache_.ReadData());
  } else {
    // And a future of a different type in this other scenario.
    return pw::async2::BoxFuture(alloc, network_.FetchData());
  }
}

Commits: 1

Simplified polling with PW_AWAIT#

PW_AWAIT is a helper macro that simplifies the process of blocking on a future that’s not ready. Example:

#include "pw_async2/await.h"

Poll<> MyReceiverTask::DoPend(Context& cx) {
  // If we don't have a future to wait on, start one.
  if (!receive_future_.is_pendable()) {
    receive_future_ = channel_.Receive();
  }

  // Wait for the future to complete. If it returns Pending,
  // this function itself will also return Pending.
  PW_AWAIT(auto result, receive_future_, cx);

  // Once ready, we can use the result.
  ProcessData(result);

  return Ready();
}

Commits: 1

Coroutine and task rework#

Allocation failures within coroutines are now easier to manage. A failure now automatically goes up to the top-level CoroTask (which crashes) or FallibleCoroTask (if you want to handle the failures). This means that you’re no longer required to return Status or Result from your coro functions and can instead use any return type, as in typical in async code from other languages. Example:

// Previously had to return `Result<int>` to account for potential
// allocation failure.
Coro<int> AsyncAdd(ValueProvider<int>& a, ValueProvider<int>& b) {
  co_return co_await a.Get() + co_await b.Get();
}

Commits: 1, 2, 3

Bluetooth#

Allocator support for buffers and packets in pw_bluetooth_sapphire#

Updated DynamicByteBuffer, CommandPacket, and EventPacket to accept an optional pw::Allocator&, allowing for more flexible memory management.

Commits: 1

C++ data structures and utilities#

Dynamically allocated maps with pw::DynamicMap#

pw::DynamicMap is a new sorted map container that uses a caller-provided pw::Allocator to dynamically allocate nodes. Example:

#include "pw_containers/dynamic_map.h"

void UseDynamicMap(pw::Allocator& alloc) {
  pw::DynamicMap<int, std::string> map(alloc);

  // Add elements to the map in all the usual ways.
  map.insert({1, "one"});
  map.emplace(2, "two");
  map[3] = "three";
}

Commits: 1, 2

New FIFO queue with performant push and pop operations#

pw::IntrusiveQueue is a new FIFO queue container that uses intrusive links to provide O(1) push and pop operations. Example:

#include "pw_containers/intrusive_queue.h"

// Elements must derive from IntrusiveQueue<T>::Item
class MyItem : public pw::IntrusiveQueue<MyItem>::Item {
 public:
  MyItem(int value) : value_(value) {}
  int value() const { return value_; }

 private:
  int value_;
};

void UseQueue() {
  pw::IntrusiveQueue<MyItem> queue;

  MyItem item1(1);
  MyItem item2(2);

  // O(1) push to back
  queue.push_back(item1);
  queue.push_back(item2);

  // O(1) pop from front
  if (!queue.empty()) {
    MyItem& front = queue.front();
    queue.pop_front();
  }
}

Commits: 1

Developer tools#

Improved pw_ide reliability#

The new ide_query tool in pw_ide provides a unified interface for querying IDE-related information from the build system, abstracting away the specifics of underlying build systems. Compile commands were updated to extract dependencies and use relative paths for better portability.

Commits: 1, 2, 3, 4

Kernel#

Async IPC#

Added channel_async_transact, channel_async_transact_complete, and channel_async_cancel syscalls to pw_kernel, enabling non-blocking IPC.

Commits: 1

Process termination and ownership#

Processes and their associated threads can now be terminated from either inside or outside the process. The new process ownership model enables waiting for threads and processes to terminate via join. Example:

// 1. Wait for all threads in the process to terminate.
for thread_ref in thread_refs {
    let thread = thread_ref.join(kernel)?;
    let _ = thread.consume();
}

// 2. Wait for the process itself to terminate.
let process = process_ref.join(kernel)?;
let _ = process.consume();

Commits: 1, 2

Reduce footprint of kernel-only applications#

The kernel no longer compiles userspace support code when building a kernel-only application, reducing binary size by over 2.6 KB on Cortex-M and RISC-V.

Note

Userspace is compiled in by default. When building a kernel-only image you need to disable userspace in the system_image.

Commits: 1

RPC#

Support multiple clients on a transfer thread in pw_transfer#

Multiple pw_transfer clients can now share a single transfer thread. Note that they can’t run transfers concurrently. Starting a transfer on one will terminate active transfers on the other. Example:

#include "pw_transfer/client.h"
#include "pw_transfer/transfer_thread.h"

void SetupTransferClients(pw::rpc::Client& rpc_client, uint32_t channel_id) {
  // Create a single transfer thread supporting 2 concurrent client transfers.
  std::array<std::byte, 64> chunk_buffer;
  std::array<std::byte, 64> encode_buffer;
  pw::transfer::Thread<2, 0> transfer_thread(chunk_buffer, encode_buffer);

  // Create multiple clients sharing the same thread.
  pw::transfer::Client client1(rpc_client, channel_id, transfer_thread);
  pw::transfer::Client client2(rpc_client, channel_id, transfer_thread);

  // Both clients can now be used independently but share the same thread.
}

Commits: 1

Toolchains and compilers#

Access toolchain tools through Bazelisk#

Because compilers, linkers, and other toolchain tools are downloaded and managed automatically via Bazel, they are part of the Bazel environment rather than your local shell environment. This means there is typically no easy way to simply run tools like nm or objdump.

To address this, Pigweed now provides wrapper targets that allow you to run the following toolchain tools interactively via bazelisk run:

  • ar,

  • c++

  • cc

  • cov

  • ld

  • nm

  • objdump

  • readelf

  • size

  • strip

Example:

$ bazelisk run --config=rp2350 //pw_toolchain/cc/current_toolchain:nm -- "$PWD/bazel-bin/pw_status/status_test"
INFO: Analyzed target //pw_toolchain/cc/current_toolchain:nm (0 packages loaded, 1 target configured).
INFO: Found 1 target...
Target //pw_toolchain/cc/current_toolchain:nm up-to-date:
  bazel-bin/pw_toolchain/cc/current_toolchain/nm
INFO: Elapsed time: 1.812s, Critical Path: 1.05s
INFO: 5 processes: 5 internal.
INFO: Build completed successfully, 5 total actions
INFO: Running command line: bazel-bin/pw_toolchain/cc/current_toolchain/nm <args omitted>
10000110 W ADC_IRQ_FIFO_Handler
10000110 W BusFault_Handler
10000110 W CLOCKS_IRQ_Handler
10000110 W DMA_IRQ_0_Handler
10000110 W DMA_IRQ_1_Handler
10000110 W DMA_IRQ_2_Handler
10000110 W DMA_IRQ_3_Handler
10000110 W DebugMon_Handler
1000011c W HardFault_Handler
00000800 a HeapSize
10000110 W I2C0_IRQ_Handler
10000110 W I2C1_IRQ_Handler
10000110 W IO_IRQ_BANK0_Handler
10000110 W IO_IRQ_BANK0_NS_Handler
10000110 W IO_IRQ_QSPI_Handler
10000110 W IO_IRQ_QSPI_NS_Handler
10000110 W MemManage_Handler
1000011a W NMI_Handler
10000110 W OTP_IRQ_Handler
# 

Commits: 1