[dev][operation] Add operation library

This library provides a generic solution to the private section problem
which exists for types such as usb_request_t, node_operation_t,
block_opt_t, and others. Specialized wrappers for each of those types
can be built on top this library.

More specifically, the problem exists whereby a series of drivers reuse
the same object as it traverses the driver stack for a specific
subsystem. There exists a public section specified by a banjo protocol,
along with a private section for each layer in driver stack appended to
the end of it like so:

---------------------
| Public Definition |
---------------------
| Driver 1 Private  |
---------------------
| Driver 2 Private  |
---------------------
|        ...        |
---------------------
| Driver N Private  |
---------------------

Driver N in this case would perform the allocation of the entire struct.
Driver 1 in the example above would be the device driver which talks
directly to hardware. The request would only be "owned" by a single
driver at a time, but only Driver N (the one who allocated the request)
would be allowed to free it.

The library is inspired by the usb::Request and friends library. They
will transition to being built on top of this library in future CLs.

Tested: `runtests -t operation-test`
Change-Id: I3ab6bdab14ef5f1654bde9817321286bf5e6eb36
diff --git a/system/dev/lib/operation/dummy.cpp b/system/dev/lib/operation/dummy.cpp
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/system/dev/lib/operation/dummy.cpp
diff --git a/system/dev/lib/operation/include/lib/operation/operation.h b/system/dev/lib/operation/include/lib/operation/operation.h
new file mode 100644
index 0000000..90e5086
--- /dev/null
+++ b/system/dev/lib/operation/include/lib/operation/operation.h
@@ -0,0 +1,506 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#pragma once
+
+#include <optional>
+#include <tuple>
+#include <utility>
+
+#include <ddk/debug.h>
+#include <fbl/algorithm.h>
+#include <fbl/auto_lock.h>
+#include <fbl/intrusive_double_list.h>
+#include <fbl/mutex.h>
+#include <fbl/unique_ptr.h>
+#include <zircon/assert.h>
+#include <zircon/compiler.h>
+
+namespace operation {
+
+// A problem exists whereby a series of drivers reuse the same object as it
+// traverses the driver stack for a specific subsystem. There exists a public
+// section specified by a banjo protocol, along with a private section for
+// each layer in driver stack appended to the end of it like so:
+//
+// ---------------------
+// | Public Definition |
+// ---------------------
+// | Driver 1 Private  |
+// ---------------------
+// | Driver 2 Private  |
+// ---------------------
+// |        ...        |
+// ---------------------
+// | Driver N Private  |
+// ---------------------
+//
+// Driver N in this case would perform the allocation of the entire struct.
+// Driver 1 in the example above would be the device driver which talks
+// directly to hardware. The request would only be "owned" by a single
+// driver at a time, but only Driver N (the one who allocated the request)
+// would be allowed to free it.
+//
+// This library provides a generic solution to the private section problem
+// which exists for types such as usb_request_t, node_operation_t,
+// block_op_t, and others. Specialized wrappers for each of those types
+// can be built on top this library.
+//
+// operation::Operation and operation::UnownedOperation provide some additional
+// safety to prevent leaks and out of bounds accesses. They will ensure that the
+// underlying buffer is returned to the caller, or delete it if the current
+// owner was responsible for allocating the request. In addition, they help
+// provide an easy way to determine the size of the request by simply specifying
+// the parent device's operation size.
+//
+// operation::OperationPool provides a simple pool that allows reuse of
+// pre-allocated operation::Operation objects.
+//
+// operation::{Unowned,}OperationQueue provide safe queues to place operations
+// in while they are pending. These queues rely on intrusive node data built
+// into the wrapper type, stored in the private storage section.
+//
+// In order to use make use of the Operation and OperationNode classes, a new
+// type must be created which inherits from it like so:
+//
+// class Foo : public Operation<Foo, OperationTraits, void>;
+// class Bar : public UnownedOperation<Bar, OperationTraits, CallbackTraits, void>;
+//
+// OperationTraits must be a type which implements the following function
+// and type signatures:
+//
+//    // Defines public definition which is wrapped.
+//    using OperationType = foo_operation_t;
+//
+//    // Performs an allocation for the operation.
+//    static OperationType* Alloc(size_t op_size);
+//
+//    // Frees the allocation created by Alloc above.
+//    static void Free(OperationType* op);
+//
+// CallbackTraits must be a type which implements the following function
+// and type signatures:
+//
+//    // The type here can be anything. It should match the callback provided to
+//    // the UnownedOperation constructor.
+//    using CallbackType = void(void* ctx, ARGS, foo_operation_t*);
+//
+//    // In case Complete is not called by UnownedOperation owners, these are
+//    // the args to trigger Complete with.
+//    static std::tuple<ARGS> AutoCompleteArgs();
+//
+//    // Should call the callback, transforming and positioning args as necessary.
+//    static void Callback(CallbackType*, void*, OperationType*, ARGS);
+//
+//    Where ARGS is a variadic number of types left to the implementer.
+//
+
+template <typename T, typename OperationTraits, typename CallbackTraits, typename Storage = void>
+class OperationNode;
+
+template <typename D, typename OperationTraits, typename CallbackTraits, typename Storage>
+class OperationBase {
+public:
+    using NodeType = OperationNode<D, OperationTraits, CallbackTraits, Storage>;
+    using OperationType = typename OperationTraits::OperationType;
+
+    OperationType* take() __WARN_UNUSED_RESULT {
+        auto* tmp = operation_;
+        operation_ = nullptr;
+        return tmp;
+    }
+
+    OperationType* operation() const {
+        return operation_;
+    }
+
+    static constexpr size_t OperationSize(size_t parent_op_size) {
+        return fbl::round_up(parent_op_size, kAlignment) + sizeof(NodeType);
+    }
+
+    size_t size() const {
+        return node_offset_ + sizeof(NodeType);
+    }
+
+    // Returns private node stored inline
+    NodeType* node() {
+        auto* node = reinterpret_cast<NodeType*>(
+            reinterpret_cast<uintptr_t>(operation_) + node_offset_);
+        return node;
+    }
+
+    Storage* private_storage() {
+        static_assert(!std::is_same<Storage, void>::value,
+                      "private_storage not available on void type.");
+        return node()->private_storage();
+    }
+
+protected:
+    OperationBase(OperationType* operation, size_t parent_op_size)
+        : operation_(operation),
+          node_offset_(fbl::round_up(parent_op_size, kAlignment)) {
+        ZX_DEBUG_ASSERT(operation != nullptr);
+    }
+
+    OperationBase(OperationBase&& other)
+        : operation_(other.operation_), node_offset_(other.node_offset_) {
+        other.operation_ = nullptr;
+    }
+
+    OperationBase& operator=(OperationBase&& other) {
+        operation_ = other.operation_;
+        node_offset_ = other.node_offset_;
+        other.operation_ = nullptr;
+        return *this;
+    }
+
+    OperationBase(const OperationBase& other) = delete;
+    OperationBase& operator=(const OperationBase& other) = delete;
+
+    OperationType* operation_;
+    zx_off_t node_offset_;
+
+private:
+    static constexpr size_t kAlignment = alignof(NodeType);
+};
+
+template <typename D, typename OperationTraits, typename Storage = void>
+class Operation : public OperationBase<D, OperationTraits, void, Storage> {
+public:
+    using BaseClass = OperationBase<D, OperationTraits, void, Storage>;
+    using NodeType = OperationNode<D, OperationTraits, void, Storage>;
+    using OperationType = typename OperationTraits::OperationType;
+
+    // Creates a new operation with payload space of data_size.
+    static std::optional<D> Alloc(size_t op_size,
+                                  size_t parent_op_size = sizeof(OperationType)) {
+        OperationType* op = OperationTraits::Alloc(op_size);
+        if (op == nullptr) {
+            return std::nullopt;
+        }
+
+        D out(op, parent_op_size);
+        new (out.node()) NodeType(out.node_offset_);
+        return std::move(out);
+    }
+
+    // Must be called with |operation| allocated via OperationTraits::Alloc.
+    Operation(OperationType* operation, size_t parent_op_size)
+        : BaseClass(operation, parent_op_size) {}
+
+    Operation(Operation&& other)
+        : BaseClass(other.operation_, other.node_offset_) {
+        other.operation_ = nullptr;
+    }
+
+    Operation& operator=(Operation&& other) {
+        BaseClass::operator=(std::move(other));
+        return *this;
+    }
+
+    ~Operation() {
+        Release();
+    }
+
+    void Release() {
+        if (BaseClass::operation_) {
+            BaseClass::node()->NodeType::~NodeType();
+            OperationTraits::Free(BaseClass::take());
+        }
+    }
+};
+
+// Similar to operation::Operation, but it doesn't call free on destruction.
+// This should be used to wrap NodeType* objects allocated in other
+// drivers.
+template <typename D, typename OperationTraits, typename CallbackTraits, typename Storage = void>
+class UnownedOperation : public OperationBase<D, OperationTraits, CallbackTraits, Storage> {
+public:
+    using BaseClass = OperationBase<D, OperationTraits, CallbackTraits, Storage>;
+    using NodeType = OperationNode<D, OperationTraits, CallbackTraits, Storage>;
+    using OperationType = typename OperationTraits::OperationType;
+    using CallbackType = typename CallbackTraits::CallbackType;
+
+    UnownedOperation(OperationType* operation, const CallbackType* complete_cb, void* cookie,
+                     size_t parent_op_size)
+        : BaseClass(operation, parent_op_size) {
+        new (BaseClass::node()) NodeType(BaseClass::node_offset_, complete_cb, cookie);
+    }
+
+    UnownedOperation(OperationType* operation, size_t parent_op_size)
+        : BaseClass(operation, parent_op_size) {
+        ZX_DEBUG_ASSERT(BaseClass::node()->node_offset() != 0);
+    }
+
+    UnownedOperation(UnownedOperation&& other)
+        : BaseClass(other.operation_, other.node_offset_) {
+        other.operation_ = nullptr;
+    }
+
+    UnownedOperation& operator=(UnownedOperation&& other) {
+        BaseClass::operator=(std::move(other));
+        return *this;
+    }
+
+    ~UnownedOperation() {
+        // Auto-complete if it wasn't.
+        auto complete = [this](auto... args) {
+            this->Complete(std::forward<decltype(args)>(args)...);
+        };
+        std::apply(complete, CallbackTraits::AutoCompleteArgs());
+    }
+
+    // Must be called by the processor when the operation has completed or failed.
+    // The operation and any virtual or physical memory obtained from it is no
+    // longer valid after Complete is called.
+    template <typename... Args>
+    void Complete(Args... args) {
+        if (BaseClass::operation_) {
+            auto* complete_cb = BaseClass::node()->complete_cb();
+            auto* cookie = BaseClass::node()->cookie();
+            BaseClass::node()->NodeType::~NodeType();
+            CallbackTraits::Callback(complete_cb, cookie, BaseClass::take(),
+                                     std::forward<Args>(args)...);
+        }
+    }
+};
+
+// Node storage for operation::Operation and operation::UnownedOperation. Does not maintain
+// ownership of underlying NodeType*. Must be transformed back into
+// appopriate wrapper type to maintain correct ownership.
+// It is strongly recommended to use operation::OperationPool and operation::OperationQueue to
+// avoid ownership pitfalls.
+template <typename T, typename OperationTraits, typename CallbackTraits, typename Storage>
+class OperationNode : public fbl::DoublyLinkedListable<
+                          OperationNode<T, OperationTraits, CallbackTraits, Storage>*> {
+public:
+    using OperationType = typename OperationTraits::OperationType;
+    using CallbackType = typename CallbackTraits::CallbackType;
+
+    OperationNode(zx_off_t node_offset, const CallbackType* complete_cb, void* cookie)
+        : node_offset_(node_offset), complete_cb_(complete_cb), cookie_(cookie) {}
+
+    ~OperationNode() = default;
+
+    T operation() const {
+        return T(
+            reinterpret_cast<OperationType*>(reinterpret_cast<uintptr_t>(this) - node_offset_),
+            node_offset_);
+    }
+
+    zx_off_t node_offset() const { return node_offset_; }
+
+    const CallbackType* complete_cb() const {
+        return complete_cb_;
+    }
+
+    void* cookie() const {
+        return cookie_;
+    }
+
+    Storage* private_storage() {
+        return &private_storage_;
+    }
+
+private:
+    const zx_off_t node_offset_;
+    const CallbackType* complete_cb_;
+    void* cookie_;
+    Storage private_storage_;
+};
+
+// Specialized version for when complete_cb is not required.
+template <typename T, typename OperationTraits, typename Storage>
+class OperationNode<T, OperationTraits, void, Storage> : public fbl::DoublyLinkedListable<
+                                                             OperationNode<T, OperationTraits, void,
+                                                                           Storage>*> {
+public:
+    using OperationType = typename OperationTraits::OperationType;
+
+    explicit OperationNode(zx_off_t node_offset)
+        : node_offset_(node_offset) {}
+
+    ~OperationNode() = default;
+
+    T operation() const {
+        return T(
+            reinterpret_cast<OperationType*>(reinterpret_cast<uintptr_t>(this) - node_offset_),
+            node_offset_);
+    }
+
+    zx_off_t node_offset() const { return node_offset_; }
+
+    Storage* private_storage() {
+        return &private_storage_;
+    }
+
+private:
+    const zx_off_t node_offset_;
+    Storage private_storage_;
+};
+
+// Specialized version for when no additional storage is required.
+template <typename T, typename OperationTraits, typename CallbackTraits>
+class OperationNode<T, OperationTraits, CallbackTraits, void> : public fbl::DoublyLinkedListable<
+                                                                    OperationNode<T,
+                                                                                  OperationTraits,
+                                                                                  CallbackTraits,
+                                                                                  void>*> {
+public:
+    using OperationType = typename OperationTraits::OperationType;
+    using CallbackType = typename CallbackTraits::CallbackType;
+
+    OperationNode(zx_off_t node_offset, const CallbackType* complete_cb, void* cookie)
+        : node_offset_(node_offset), complete_cb_(complete_cb), cookie_(cookie) {}
+
+    ~OperationNode() = default;
+
+    T operation() const {
+        return T(
+            reinterpret_cast<OperationType*>(reinterpret_cast<uintptr_t>(this) - node_offset_),
+            node_offset_);
+    }
+
+    zx_off_t node_offset() const { return node_offset_; }
+
+    const CallbackType* complete_cb() const {
+        return complete_cb_;
+    }
+
+    void* cookie() const {
+        return cookie_;
+    }
+
+private:
+    const zx_off_t node_offset_;
+    const CallbackType* complete_cb_;
+    void* cookie_;
+};
+
+// Specialized version for when no additional storage is required, and
+// complete_cb is not required.
+template <typename T, typename OperationTraits>
+class OperationNode<T, OperationTraits, void, void> : public fbl::DoublyLinkedListable<
+                                                          OperationNode<T, OperationTraits, void,
+                                                                        void>*> {
+public:
+    using OperationType = typename OperationTraits::OperationType;
+
+    OperationNode(zx_off_t node_offset)
+        : node_offset_(node_offset) {}
+
+    ~OperationNode() = default;
+
+    T operation() const {
+        return T(
+            reinterpret_cast<OperationType*>(reinterpret_cast<uintptr_t>(this) - node_offset_),
+            node_offset_);
+    }
+
+    zx_off_t node_offset() const { return node_offset_; }
+
+private:
+    const zx_off_t node_offset_;
+};
+
+// Convenience queue wrapper around fbl::DoublyLinkedList<T>.
+// The class is thread-safe.
+template <typename OpType, typename OperationTraits, typename CallbackTraits, typename Storage>
+class BaseQueue {
+public:
+    using NodeType = OperationNode<OpType, OperationTraits, CallbackTraits, Storage>;
+
+    DECLARE_HAS_MEMBER_FN_WITH_SIGNATURE(has_node, node, NodeType* (C::*)());
+    static_assert(has_node<OpType>::value,
+                  "OpType must implement OperationNode<OpType, OperationTraits, CallbackTraits, "
+                  "Storage>* node()");
+
+    BaseQueue() {}
+
+    ~BaseQueue() {
+        Release();
+    }
+
+    BaseQueue(BaseQueue&& other) {
+        fbl::AutoLock al(&other.lock_);
+        queue_.swap(other.queue_);
+    }
+
+    BaseQueue& operator=(BaseQueue&& other) {
+        fbl::AutoLock al1(&lock_);
+        fbl::AutoLock al2(&other.lock_);
+        queue_.clear();
+        queue_.swap(other.queue_);
+        return *this;
+    }
+
+    void push(OpType op) {
+        fbl::AutoLock al(&lock_);
+        auto* node = op.node();
+        queue_.push_front(node);
+        // Must prevent Complete/Release from being called in destructor.
+        __UNUSED auto dummy = op.take();
+    }
+
+    void push_next(OpType op) {
+        fbl::AutoLock al(&lock_);
+        auto* node = op.node();
+        queue_.push_back(node);
+        // Must prevent Complete/Release from being called in destructor.
+        __UNUSED auto dummy = op.take();
+    }
+
+    std::optional<OpType> pop() {
+        fbl::AutoLock al(&lock_);
+        auto* node = queue_.pop_back();
+        if (node) {
+            return std::move(node->operation());
+        }
+        return std::nullopt;
+    }
+
+    bool is_empty() {
+        fbl::AutoLock al(&lock_);
+        return queue_.is_empty();
+    }
+
+    // Releases all ops stored in the queue.
+    void Release() {
+        fbl::AutoLock al(&lock_);
+        while (!queue_.is_empty()) {
+            // Tranform back into operation to force correct destructor to run.
+            __UNUSED auto op = queue_.pop_back()->operation();
+        }
+    }
+
+protected:
+    fbl::Mutex lock_;
+    fbl::DoublyLinkedList<NodeType*> queue_ __TA_GUARDED(lock_);
+};
+
+template <typename D, typename OperationTraits, typename CallbackTraits, typename Storage = void>
+using UnownedOperationQueue = BaseQueue<D, OperationTraits, CallbackTraits, Storage>;
+
+template <typename D, typename OperationTraits, typename Storage = void>
+using OperationQueue = BaseQueue<D, OperationTraits, void, Storage>;
+
+// A driver may use operation::OperationPool for recycling their own operations.
+template <typename OpType, typename OperationTraits, typename Storage = void>
+class OperationPool : protected BaseQueue<OpType, OperationTraits, void, Storage> {
+public:
+    using BaseClass = BaseQueue<OpType, OperationTraits, void, Storage>;
+
+    // Use constructor.
+    using BaseClass::BaseClass;
+
+    // Operate like a stack rather than a queue.
+    void push(OpType op) {
+        BaseClass::push_next(std::forward<OpType>(op));
+    }
+    using BaseClass::pop;
+
+    using BaseClass::Release;
+};
+
+} // namespace operation
diff --git a/system/dev/lib/operation/rules.mk b/system/dev/lib/operation/rules.mk
new file mode 100644
index 0000000..7974173
--- /dev/null
+++ b/system/dev/lib/operation/rules.mk
@@ -0,0 +1,18 @@
+# Copyright 2019 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+LOCAL_DIR := $(GET_LOCAL_DIR)
+
+MODULE := $(LOCAL_DIR)
+
+MODULE_TYPE := userlib
+
+MODULE_SRCS := \
+    $(LOCAL_DIR)/dummy.cpp \
+
+MODULE_STATIC_LIBS := \
+    system/ulib/fbl \
+    system/ulib/zx \
+
+include make/module.mk
diff --git a/system/dev/test/operation/main.cpp b/system/dev/test/operation/main.cpp
new file mode 100644
index 0000000..8af51d4
--- /dev/null
+++ b/system/dev/test/operation/main.cpp
@@ -0,0 +1,10 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+#include <lib/operation/operation.h>
+
+#include <unittest/unittest.h>
+
+int main(int argc, char** argv) {
+    return unittest_run_all_tests(argc, argv) ? 0 : -1;
+}
diff --git a/system/dev/test/operation/operation-pool-test.cpp b/system/dev/test/operation/operation-pool-test.cpp
new file mode 100644
index 0000000..6ce1b66
--- /dev/null
+++ b/system/dev/test/operation/operation-pool-test.cpp
@@ -0,0 +1,129 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <lib/operation/operation.h>
+
+#include <unittest/unittest.h>
+
+namespace {
+
+struct TestOp {
+    int dummy;
+};
+
+struct TestOpTraits {
+    using OperationType = TestOp;
+
+    static OperationType* Alloc(size_t op_size) {
+        fbl::AllocChecker ac;
+        fbl::unique_ptr<uint8_t[]> raw;
+        if constexpr (alignof(OperationType) > __STDCPP_DEFAULT_NEW_ALIGNMENT__) {
+            raw = fbl::unique_ptr<uint8_t[]>(
+                new (static_cast<std::align_val_t>(alignof(OperationType)), &ac) uint8_t[op_size]);
+        } else {
+            raw = fbl::unique_ptr<uint8_t[]>(new (&ac) uint8_t[op_size]);
+        }
+        if (!ac.check()) {
+            return nullptr;
+        }
+        return reinterpret_cast<TestOp*>(raw.release());
+    }
+
+    static void Free(OperationType* op) {
+        delete[] reinterpret_cast<uint8_t*>(op);
+    }
+};
+
+using TestOpCallback = void (*)(void*, zx_status_t, TestOp*);
+
+struct CallbackTraits {
+    using CallbackType = TestOpCallback;
+
+    static std::tuple<zx_status_t> AutoCompleteArgs() {
+        return std::make_tuple(ZX_ERR_INTERNAL);
+    }
+
+    static void Callback(const CallbackType* callback, void* cookie, TestOp* op,
+                         zx_status_t status) {
+        (*callback)(cookie, status, op);
+    }
+};
+
+struct Operation : public operation::Operation<Operation, TestOpTraits, void> {
+    using BaseClass = operation::Operation<Operation, TestOpTraits, void>;
+    using BaseClass::BaseClass;
+};
+
+struct UnownedOperation : public operation::UnownedOperation<UnownedOperation, TestOpTraits,
+                                                             CallbackTraits, void> {
+
+    using BaseClass = operation::UnownedOperation<UnownedOperation, TestOpTraits,
+                                                  CallbackTraits, void>;
+    using BaseClass::BaseClass;
+};
+
+using OperationPool = operation::OperationPool<Operation, TestOpTraits, void>;
+
+constexpr size_t kParentOpSize = sizeof(TestOp);
+constexpr size_t kOpSize = Operation::OperationSize(kParentOpSize);
+
+bool TrivialLifetimeTest() {
+    BEGIN_TEST;
+    OperationPool pool;
+    END_TEST;
+}
+
+bool SingleOperationTest() {
+    BEGIN_TEST;
+    std::optional<Operation> operation = Operation::Alloc(kOpSize);
+    ASSERT_TRUE(operation.has_value());
+
+    OperationPool pool;
+    EXPECT_TRUE(pool.pop() == std::nullopt);
+    pool.push(std::move(*operation));
+    EXPECT_TRUE(pool.pop() != std::nullopt);
+    EXPECT_TRUE(pool.pop() == std::nullopt);
+    END_TEST;
+}
+
+bool MultipleOperationTest() {
+    BEGIN_TEST;
+    OperationPool pool;
+
+    for (size_t i = 0; i < 10; i++) {
+        std::optional<Operation> operation = Operation::Alloc(kOpSize);
+        ASSERT_TRUE(operation.has_value());
+        pool.push(std::move(*operation));
+    }
+
+    for (size_t i = 0; i < 10; i++) {
+        EXPECT_TRUE(pool.pop() != std::nullopt);
+    }
+    EXPECT_TRUE(pool.pop() == std::nullopt);
+    END_TEST;
+}
+
+bool ReleaseTest() {
+    BEGIN_TEST;
+    OperationPool pool;
+
+    for (size_t i = 0; i < 10; i++) {
+        std::optional<Operation> operation = Operation::Alloc(kOpSize);
+        ASSERT_TRUE(operation.has_value());
+        pool.push(std::move(*operation));
+    }
+
+    pool.Release();
+    EXPECT_TRUE(pool.pop() == std::nullopt);
+    END_TEST;
+}
+
+} // namespace
+
+BEGIN_TEST_CASE(OperationPoolTests)
+RUN_TEST_SMALL(TrivialLifetimeTest)
+RUN_TEST_SMALL(SingleOperationTest)
+RUN_TEST_SMALL(MultipleOperationTest)
+RUN_TEST_SMALL(ReleaseTest)
+END_TEST_CASE(OperationPoolTests)
diff --git a/system/dev/test/operation/operation-queue-test.cpp b/system/dev/test/operation/operation-queue-test.cpp
new file mode 100644
index 0000000..55b7c54
--- /dev/null
+++ b/system/dev/test/operation/operation-queue-test.cpp
@@ -0,0 +1,278 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <lib/operation/operation.h>
+
+#include <unittest/unittest.h>
+
+namespace {
+
+struct TestOp {
+    int dummy;
+};
+
+struct TestOpTraits {
+    using OperationType = TestOp;
+
+    static OperationType* Alloc(size_t op_size) {
+        fbl::AllocChecker ac;
+        fbl::unique_ptr<uint8_t[]> raw;
+        if constexpr (alignof(OperationType) > __STDCPP_DEFAULT_NEW_ALIGNMENT__) {
+            raw = fbl::unique_ptr<uint8_t[]>(
+                new (static_cast<std::align_val_t>(alignof(OperationType)), &ac) uint8_t[op_size]);
+        } else {
+            raw = fbl::unique_ptr<uint8_t[]>(new (&ac) uint8_t[op_size]);
+        }
+        if (!ac.check()) {
+            return nullptr;
+        }
+        return reinterpret_cast<TestOp*>(raw.release());
+    }
+
+    static void Free(OperationType* op) {
+        delete[] reinterpret_cast<uint8_t*>(op);
+    }
+};
+
+using TestOpCallback = void (*)(void*, zx_status_t, TestOp*);
+
+struct CallbackTraits {
+    using CallbackType = TestOpCallback;
+
+    static std::tuple<zx_status_t> AutoCompleteArgs() {
+        return std::make_tuple(ZX_ERR_INTERNAL);
+    }
+
+    static void Callback(const CallbackType* callback, void* cookie, TestOp* op,
+                         zx_status_t status) {
+        (*callback)(cookie, status, op);
+    }
+};
+
+struct Operation : public operation::Operation<Operation, TestOpTraits, void> {
+    using BaseClass = operation::Operation<Operation, TestOpTraits, void>;
+    using BaseClass::BaseClass;
+};
+
+struct UnownedOperation : public operation::UnownedOperation<UnownedOperation, TestOpTraits,
+                                                             CallbackTraits, void> {
+    using BaseClass = operation::UnownedOperation<UnownedOperation, TestOpTraits,
+                                                  CallbackTraits, void>;
+    using BaseClass::BaseClass;
+};
+
+using OperationQueue = operation::OperationQueue<Operation, TestOpTraits, void>;
+using UnownedOperationQueue = operation::UnownedOperationQueue<UnownedOperation, TestOpTraits,
+                                                               CallbackTraits, void>;
+
+constexpr size_t kParentOpSize = sizeof(TestOp);
+constexpr size_t kOpSize = Operation::OperationSize(kParentOpSize);
+
+bool TrivialLifetimeTest() {
+    BEGIN_TEST;
+    OperationQueue queue;
+    UnownedOperationQueue unowned_queue;
+    END_TEST;
+}
+
+bool SingleOperationTest() {
+    BEGIN_TEST;
+    std::optional<Operation> operation = Operation::Alloc(kOpSize);
+    ASSERT_TRUE(operation.has_value());
+
+    OperationQueue queue;
+    EXPECT_TRUE(queue.pop() == std::nullopt);
+    queue.push(std::move(*operation));
+    EXPECT_TRUE(queue.pop() != std::nullopt);
+    EXPECT_TRUE(queue.pop() == std::nullopt);
+    END_TEST;
+}
+
+bool MultipleOperationTest() {
+    BEGIN_TEST;
+    OperationQueue queue;
+
+    for (size_t i = 0; i < 10; i++) {
+        std::optional<Operation> operation = Operation::Alloc(kOpSize);
+        ASSERT_TRUE(operation.has_value());
+        queue.push(std::move(*operation));
+    }
+
+    for (size_t i = 0; i < 10; i++) {
+        EXPECT_TRUE(queue.pop() != std::nullopt);
+    }
+    EXPECT_TRUE(queue.pop() == std::nullopt);
+    END_TEST;
+}
+
+bool ReleaseTest() {
+    BEGIN_TEST;
+    OperationQueue queue;
+
+    for (size_t i = 0; i < 10; i++) {
+        std::optional<Operation> operation = Operation::Alloc(kOpSize);
+        ASSERT_TRUE(operation.has_value());
+        queue.push(std::move(*operation));
+    }
+
+    queue.Release();
+    EXPECT_TRUE(queue.pop() == std::nullopt);
+    END_TEST;
+}
+
+bool MultipleLayerTest() {
+    BEGIN_TEST;
+
+    using FirstLayerOp = UnownedOperation;
+    using SecondLayerOp = Operation;
+
+    constexpr size_t kBaseOpSize = sizeof(TestOp);
+    constexpr size_t kFirstLayerOpSize = FirstLayerOp::OperationSize(kBaseOpSize);
+    constexpr size_t kSecondLayerOpSize = SecondLayerOp::OperationSize(kFirstLayerOpSize);
+
+    OperationQueue queue;
+    for (size_t i = 0; i < 10; i++) {
+        std::optional<SecondLayerOp> operation = SecondLayerOp::Alloc(kSecondLayerOpSize,
+                                                                      kFirstLayerOpSize);
+        ASSERT_TRUE(operation.has_value());
+        queue.push(std::move(*operation));
+    }
+
+    UnownedOperationQueue queue2;
+    size_t count = 0;
+    for (auto operation = queue.pop(); operation; operation = queue.pop()) {
+        FirstLayerOp unowned(operation->take(), nullptr, nullptr, kBaseOpSize);
+        queue2.push(std::move(unowned));
+        ++count;
+    }
+    EXPECT_EQ(count, 10);
+
+    count = 0;
+    for (auto unowned = queue2.pop(); unowned; unowned = queue2.pop()) {
+        SecondLayerOp operation(unowned->take(), kFirstLayerOpSize);
+        queue.push(std::move(operation));
+        ++count;
+    }
+    EXPECT_EQ(count, 10);
+
+    END_TEST;
+}
+
+bool MultipleLayerWithStorageTest() {
+    BEGIN_TEST;
+
+    struct FirstLayerOp : public operation::UnownedOperation<FirstLayerOp, TestOpTraits,
+                                                             CallbackTraits, char> {
+
+        using BaseClass = operation::UnownedOperation<FirstLayerOp, TestOpTraits,
+                                                      CallbackTraits, char>;
+        using BaseClass::BaseClass;
+    };
+
+    struct SecondLayerOp : public operation::Operation<SecondLayerOp, TestOpTraits, uint64_t> {
+        using BaseClass = operation::Operation<SecondLayerOp, TestOpTraits, uint64_t>;
+        using BaseClass::BaseClass;
+    };
+
+    constexpr size_t kBaseOpSize = sizeof(TestOp);
+    constexpr size_t kFirstLayerOpSize = FirstLayerOp::OperationSize(kBaseOpSize);
+    constexpr size_t kSecondLayerOpSize = SecondLayerOp::OperationSize(kFirstLayerOpSize);
+
+    operation::OperationQueue<SecondLayerOp, TestOpTraits, uint64_t> queue;
+    for (size_t i = 0; i < 10; i++) {
+        std::optional<SecondLayerOp> operation = SecondLayerOp::Alloc(kSecondLayerOpSize,
+                                                                      kFirstLayerOpSize);
+        ASSERT_TRUE(operation.has_value());
+        *operation->private_storage() = i;
+        EXPECT_EQ(*operation->private_storage(), i);
+        queue.push(std::move(*operation));
+    }
+
+    operation::UnownedOperationQueue<FirstLayerOp, TestOpTraits, CallbackTraits, char> queue2;
+    size_t count = 0;
+    for (auto operation = queue.pop(); operation; operation = queue.pop()) {
+        FirstLayerOp unowned(operation->take(), nullptr, nullptr, kBaseOpSize);
+        *unowned.private_storage() = static_cast<char>('a' + count);
+        queue2.push(std::move(unowned));
+        ++count;
+    }
+    EXPECT_EQ(count, 10);
+
+    count = 0;
+    for (auto unowned = queue2.pop(); unowned; unowned = queue2.pop()) {
+        EXPECT_EQ(*unowned->private_storage(), static_cast<char>('a' + count));
+        SecondLayerOp operation(unowned->take(), kFirstLayerOpSize);
+        EXPECT_EQ(*operation.private_storage(), count);
+        queue.push(std::move(operation));
+        ++count;
+    }
+    EXPECT_EQ(count, 10);
+
+    END_TEST;
+}
+
+bool MultipleLayerWithCallbackTest() {
+    BEGIN_TEST;
+
+    struct FirstLayerOp : public operation::UnownedOperation<FirstLayerOp, TestOpTraits,
+                                                             CallbackTraits, char> {
+        using BaseClass = operation::UnownedOperation<FirstLayerOp, TestOpTraits,
+                                                      CallbackTraits, char>;
+        using BaseClass::BaseClass;
+    };
+
+    struct SecondLayerOp : public operation::Operation<SecondLayerOp, TestOpTraits, uint64_t> {
+        using BaseClass = operation::Operation<SecondLayerOp, TestOpTraits, uint64_t>;
+        using BaseClass::BaseClass;
+    };
+
+    constexpr size_t kBaseOpSize = sizeof(TestOp);
+    constexpr size_t kFirstLayerOpSize = FirstLayerOp::OperationSize(kBaseOpSize);
+    constexpr size_t kSecondLayerOpSize = SecondLayerOp::OperationSize(kFirstLayerOpSize);
+
+    operation::OperationQueue<SecondLayerOp, TestOpTraits, uint64_t> queue;
+    for (size_t i = 0; i < 10; i++) {
+        std::optional<SecondLayerOp> operation = SecondLayerOp::Alloc(kSecondLayerOpSize,
+                                                                      kFirstLayerOpSize);
+        ASSERT_TRUE(operation.has_value());
+        *operation->private_storage() = i;
+        EXPECT_EQ(*operation->private_storage(), i);
+        queue.push(std::move(*operation));
+    }
+
+    auto callback = [](void* ctx, zx_status_t status, TestOp* operation) {
+        auto* queue = static_cast<operation::OperationQueue<SecondLayerOp, TestOpTraits, uint64_t>*>(ctx);
+        queue->push(SecondLayerOp(operation, kFirstLayerOpSize));
+    };
+    TestOpCallback cb = callback;
+
+    {
+        operation::UnownedOperationQueue<FirstLayerOp, TestOpTraits, CallbackTraits, char> queue2;
+        for (auto operation = queue.pop(); operation; operation = queue.pop()) {
+            FirstLayerOp unowned(operation->take(), &cb, &queue, kBaseOpSize);
+            queue2.push(std::move(unowned));
+        }
+    }
+
+    size_t count = 0;
+    for (auto operation = queue.pop(); operation; operation = queue.pop()) {
+        EXPECT_EQ(*operation->private_storage(), count);
+        ++count;
+    }
+    EXPECT_EQ(count, 10);
+
+    END_TEST;
+}
+
+} // namespace
+
+BEGIN_TEST_CASE(OperationQueueTests)
+RUN_TEST_SMALL(TrivialLifetimeTest)
+RUN_TEST_SMALL(SingleOperationTest)
+RUN_TEST_SMALL(MultipleOperationTest)
+RUN_TEST_SMALL(ReleaseTest)
+RUN_TEST_SMALL(MultipleLayerTest)
+RUN_TEST_SMALL(MultipleLayerWithStorageTest)
+RUN_TEST_SMALL(MultipleLayerWithCallbackTest)
+END_TEST_CASE(OperationQueueTests);
diff --git a/system/dev/test/operation/operation-test.cpp b/system/dev/test/operation/operation-test.cpp
new file mode 100644
index 0000000..07a822d
--- /dev/null
+++ b/system/dev/test/operation/operation-test.cpp
@@ -0,0 +1,172 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <lib/operation/operation.h>
+
+#include <fbl/algorithm.h>
+#include <fbl/auto_call.h>
+#include <unittest/unittest.h>
+
+namespace {
+
+struct TestOp {
+    int dummy;
+};
+
+struct TestOpTraits {
+    using OperationType = TestOp;
+
+    static OperationType* Alloc(size_t op_size) {
+        fbl::AllocChecker ac;
+        fbl::unique_ptr<uint8_t[]> raw;
+        if constexpr (alignof(OperationType) > __STDCPP_DEFAULT_NEW_ALIGNMENT__) {
+            raw = fbl::unique_ptr<uint8_t[]>(
+                new (static_cast<std::align_val_t>(alignof(OperationType)), &ac) uint8_t[op_size]);
+        } else {
+            raw = fbl::unique_ptr<uint8_t[]>(new (&ac) uint8_t[op_size]);
+        }
+        if (!ac.check()) {
+            return nullptr;
+        }
+        return reinterpret_cast<TestOp*>(raw.release());
+    }
+
+    static void Free(OperationType* op) {
+        delete[] reinterpret_cast<uint8_t*>(op);
+    }
+};
+
+using TestOpCallback = void (*)(void*, zx_status_t, TestOp*);
+
+struct CallbackTraits {
+    using CallbackType = TestOpCallback;
+
+    static std::tuple<zx_status_t> AutoCompleteArgs() {
+        return std::make_tuple(ZX_ERR_INTERNAL);
+    }
+
+    static void Callback(const CallbackType* callback, void* cookie, TestOp* op,
+                         zx_status_t status) {
+        (*callback)(cookie, status, op);
+    }
+};
+
+struct Operation : public operation::Operation<Operation, TestOpTraits, void> {
+    using BaseClass = operation::Operation<Operation, TestOpTraits, void>;
+    using BaseClass::BaseClass;
+};
+
+struct UnownedOperation : public operation::UnownedOperation<UnownedOperation, TestOpTraits,
+                                                            CallbackTraits, void> {
+
+    using BaseClass = operation::UnownedOperation<UnownedOperation, TestOpTraits,
+                                                  CallbackTraits, void>;
+    using BaseClass::BaseClass;
+};
+
+constexpr size_t kParentOpSize = sizeof(TestOp);
+constexpr size_t kOpSize = Operation::OperationSize(kParentOpSize);
+
+bool AllocTest() {
+    BEGIN_TEST;
+    std::optional<Operation> op = Operation::Alloc(kOpSize);
+    EXPECT_TRUE(op.has_value());
+    END_TEST;
+}
+
+bool PrivateStorageTest() {
+    BEGIN_TEST;
+
+    struct Private : public operation::Operation<Private, TestOpTraits, uint32_t> {
+          using BaseClass = operation::Operation<Private, TestOpTraits, uint32_t>;
+          using BaseClass::BaseClass;
+    };
+
+    constexpr size_t kOperationSize = Private::OperationSize(kParentOpSize);
+    auto operation = Private::Alloc(kOperationSize);
+    ASSERT_TRUE(operation.has_value());
+    *operation->private_storage() = 1001;
+    ASSERT_EQ(*operation->private_storage(), 1001);
+    END_TEST;
+}
+
+bool MultipleSectionTest() {
+    BEGIN_TEST;
+
+    constexpr size_t kBaseOpSize = sizeof(TestOp);
+    constexpr size_t kFirstLayerOpSize = Operation::OperationSize(kBaseOpSize);
+    constexpr size_t kSecondLayerOpSize =
+        UnownedOperation::OperationSize(kFirstLayerOpSize);
+    constexpr size_t kThirdLayerOpSize =
+        UnownedOperation::OperationSize(kSecondLayerOpSize);
+
+    std::optional<Operation> operation = Operation::Alloc(kThirdLayerOpSize);
+    ASSERT_TRUE(operation.has_value());
+
+    UnownedOperation operation2(operation->take(), nullptr, nullptr, kFirstLayerOpSize);
+    UnownedOperation operation3(operation2.take(), nullptr, nullptr, kSecondLayerOpSize);
+    operation = Operation(operation3.take(), kBaseOpSize);
+
+    END_TEST;
+}
+
+bool CallbackTest() {
+    BEGIN_TEST;
+    constexpr size_t kBaseOpSize = sizeof(TestOp);
+    constexpr size_t kFirstLayerOpSize = Operation::OperationSize(kBaseOpSize);
+    constexpr size_t kSecondLayerOpSize =
+        UnownedOperation::OperationSize(kFirstLayerOpSize);
+
+    bool called = false;
+    auto callback = [](void* ctx, zx_status_t st, TestOp* operation) -> void {
+        *static_cast<bool*>(ctx) = true;
+        // We take ownership.
+        Operation unused(operation, kBaseOpSize);
+    };
+    TestOpCallback cb = callback;
+    std::optional<Operation> operation = Operation::Alloc(kSecondLayerOpSize);
+    ASSERT_TRUE(operation.has_value());
+
+    UnownedOperation operation2(operation->take(), &cb, &called, kFirstLayerOpSize);
+    operation2.Complete(ZX_OK);
+    EXPECT_TRUE(called);
+
+    END_TEST;
+}
+
+bool AutoCallbackTest() {
+    BEGIN_TEST;
+    constexpr size_t kBaseOpSize = sizeof(TestOp);
+    constexpr size_t kFirstLayerOpSize = Operation::OperationSize(kBaseOpSize);
+    constexpr size_t kSecondLayerOpSize =
+        UnownedOperation::OperationSize(kFirstLayerOpSize);
+
+    bool called = false;
+    auto callback = [](void* ctx, zx_status_t st, TestOp* operation) {
+        *static_cast<bool*>(ctx) = true;
+        // We take ownership.
+        Operation unused(operation, kBaseOpSize);
+    };
+    TestOpCallback cb = callback;
+
+    std::optional<Operation> operation = Operation::Alloc(kSecondLayerOpSize);
+    ASSERT_TRUE(operation.has_value());
+
+    {
+        UnownedOperation operation2(operation->take(), &cb, &called, kFirstLayerOpSize);
+    }
+    EXPECT_TRUE(called);
+
+    END_TEST;
+}
+
+} // namespace
+
+BEGIN_TEST_CASE(OperationTests)
+RUN_TEST_SMALL(AllocTest)
+RUN_TEST_SMALL(PrivateStorageTest)
+RUN_TEST_SMALL(MultipleSectionTest)
+RUN_TEST_SMALL(CallbackTest)
+RUN_TEST_SMALL(AutoCallbackTest)
+END_TEST_CASE(OperationTests);
diff --git a/system/dev/test/operation/rules.mk b/system/dev/test/operation/rules.mk
new file mode 100644
index 0000000..1fba94d
--- /dev/null
+++ b/system/dev/test/operation/rules.mk
@@ -0,0 +1,32 @@
+# Copyright 2019 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+LOCAL_DIR := $(GET_LOCAL_DIR)
+
+MODULE := $(LOCAL_DIR).test
+
+MODULE_TYPE := usertest
+
+MODULE_NAME := operation-test
+
+MODULE_SRCS += \
+    $(LOCAL_DIR)/operation-test.cpp \
+    $(LOCAL_DIR)/operation-pool-test.cpp \
+    $(LOCAL_DIR)/operation-queue-test.cpp \
+    $(LOCAL_DIR)/main.cpp \
+
+MODULE_STATIC_LIBS := \
+    system/dev/lib/fake_ddk \
+    system/dev/lib/operation \
+    system/ulib/ddk \
+    system/ulib/fbl \
+    system/ulib/zxcpp \
+
+MODULE_LIBS := \
+    system/ulib/c \
+    system/ulib/fdio \
+    system/ulib/unittest \
+    system/ulib/zircon \
+
+include make/module.mk