[API Proposal]: SpanPool API for safer, scoped buffer pooling
- Dominant language
- C#
- Stars
- 18.3k
- Forks
- 5.6k
- PR merge metrics
- PR metrics pending
Description
### Background and motivation
`ArrayPool` is a powerful API for reducing allocations by reusing arrays. However, it has some inherent safety issues:
- Users can forget to return arrays to the pool, causing memory leaks
- Arrays can be accidentally used after being returned to the pool
- The rented array size may be larger than requested, requiring careful bounds checking
- Defaults to uninitialized memory, which can present risks around undefined behavior
The proposed `SpanPool` API addresses these issues by providing a scoped, callback-based API that ensures automatic return of buffers and prevents use-after-return by design.
### API Proposal
```csharp
namespace System.Buffers
{
public static class SpanPool
{
///
/// Rents a buffer of at least the specified size and invokes the provided action with a span of the given length.
/// The buffer is automatically returned to the pool when the action completes.
///
/// The type of elements in the span.
/// The length of the span required.
/// The action to invoke with the rented span.
/// Thrown when is less than 0.
public static void Rent(int len, Action> action);
///
/// Rents a buffer of at least the specified size and invokes the provided action with a span over the buffer.
/// The buffer is automatically returned to the pool when the action completes.
///
/// The type of elements in the span.
/// The minimum length of the span required.
/// The action to invoke with the rented span.
/// Thrown when is less than 0.
public static TReturn Rent(int len, Func, TReturn> action);
}
```
### API Usage
### Basic Usage
```csharp
// Process data in a temporary buffer
SpanPool.Rent(1024, span =>
{
// Use the span here
ProcessData(span);
// Buffer is automatically returned when we exit this scope
});
```
### Returning a Value
```csharp
string result = SpanPool.Rent(256, span =>
{
// Build a string using the temporary buffer
int written = FormatData(span);
return new string(span.Slice(0, written));
});
```
### Alternative Designs
We could also support an interface-based version to avoid allocation. Given the interface definition:
```csharp
interface IAction where T : allows ref struct
{
void Invoke();
}
```
you could define an overload:
```csharp
static class SpanPool
{
public static void Rent(int len, TAction action) where TAction : IAction>;
}
```
When `Rent` is called with `TAction` implemented with a struct, the code is specialized for every call site. There are two main drawbacks:
1. Roslyn doesn't support this pattern for lambda syntax and capturing, so the user would have to write it out manually.
2. This is a complicated and expensive way to achieve our goal (inlining the lambda). Ideally we wouldn't need to create a bunch of extra machinery and potentially bad JIT/type load side effects just to achieve inlining.
### Risks
1. **Risk**: Performance overhead from delegate invocation
- **Mitigation**: JIT optimizations for delegate inlining, state-passing overloads
2. **Risk**: Users still needing ArrayPool for scenarios where the lifetime is not well-scoped
- **Mitigation**: This complements, not replaces, ArrayPool
Contributor guide
Assessment
This issue has not been assessed yet.