How to Implement a Custom ASIO I/O Object: A Complete Guide

To implement a custom ASIO I/O object, you must define a service class containing the low-level implementation, then derive a public wrapper class from asio::basic_io_object<YourService> that exposes high-level operations.

ASIO's extensibility relies on the service / I/O object pattern, which allows you to integrate custom asynchronous resources into the ASIO ecosystem. According to the chriskohlhoff/asio source code, this architecture separates the user-facing interface from the platform-specific implementation, enabling seamless integration with io_context, executors, and completion tokens.

Understanding the Service/Object Architecture

ASIO implements all I/O objects using a two-layer design. The service manages the actual native resources (file descriptors, sockets, timers), while the I/O object provides the user API and lifetime management.

In include/asio/basic_io_object.hpp, the asio::basic_io_object template class handles the generic plumbing: storing a reference to the service, forwarding get_executor(), and managing the implementation's lifetime. Your custom object inherits from this base class, which automatically handles construction, destruction, and move semantics through the service interface.

Reference implementations like basic_socket in include/asio/basic_socket.hpp demonstrate this pattern in production code, showing how built-in objects delegate operations to their associated services.

Defining the Service Class

The service is the core of your custom I/O object. It must define how native resources are created, destroyed, and moved between object instances.

Required Service Members

Every service must contain these specific members:

  • using implementation_type = ...; – The concrete type holding native resources (e.g., file descriptors).
  • void construct(implementation_type&) – Initializes the implementation (opens descriptors, allocates buffers).
  • void destroy(implementation_type&) – Cleans up resources (closes descriptors, frees memory).
  • asio::io_context& get_io_context() – Returns the associated io_context (typically stored as a member).

Optional Move Semantics

For moveable I/O objects, implement these additional members:

  • void move_construct(implementation_type& dest, implementation_type& src) – Transfers ownership during move-construction.
  • void move_assign(implementation_type& dest, service_type&, implementation_type& src) – Transfers ownership during move-assignment.

Here is a minimal service implementation:

#pragma once
#include <asio.hpp>
#include <atomic>
#include <cstdio>

namespace my_namespace {

struct my_custom_service
{
  // The concrete implementation that holds a native resource.
  struct implementation_type
  {
    int native_handle = -1;          // Example: a POSIX file descriptor.
  };

  // Called by `basic_io_object` ctor.
  void construct(implementation_type& impl)
  {
    impl.native_handle = ::open("/dev/null", O_WRONLY);
    std::printf("my_custom_service::construct – fd=%d\n", impl.native_handle);
  }

  // Called by `basic_io_object` dtor.
  void destroy(implementation_type& impl)
  {
    if (impl.native_handle != -1)
    {
      ::close(impl.native_handle);
      std::printf("my_custom_service::destroy – fd closed\n");
    }
  }

  // Optional – enable move‑construction of the I/O object.
  static constexpr bool is_movable = true;

  void move_construct(implementation_type& dest,
                      implementation_type& src) noexcept
  {
    dest.native_handle = src.native_handle;
    src.native_handle = -1;
  }

  void move_assign(implementation_type& dest,
                   my_custom_service&,
                   implementation_type& src) noexcept
  {
    // Simple implementation: destroy old, then move‑construct.
    destroy(dest);
    move_construct(dest, src);
  }

  // Utility to access the io_context from the service.
  asio::io_context& get_io_context() const { return io_context_; }

  explicit my_custom_service(asio::io_context& ctx) : io_context_(ctx) {}

private:
  asio::io_context& io_context_;
};

} // namespace my_namespace

The service automatically registers with the io_context through asio::use_service, which creates one service instance per io_context.

Creating the Public I/O Object Wrapper

Derive your public class from asio::basic_io_object<YourService>. This base class provides get_io_context(), get_executor(), and protected access to get_implementation().

#pragma once
#include <asio.hpp>
#include "my_custom_service.hpp"

namespace my_namespace {

class my_custom_io
  : public asio::basic_io_object<my_custom_service>
{
public:
  // Expose the service's implementation for internal helpers.
  using implementation_type = service_type::implementation_type;

  // ctor forwards the io_context to the base class.
  explicit my_custom_io(asio::io_context& ctx)
    : asio::basic_io_object<my_custom_service>(ctx)
  {}

  // Example synchronous operation.
  void write_some(const char* data, std::size_t size)
  {
    const implementation_type& impl = this->get_implementation();
    ::write(impl.native_handle, data, size);
  }

  // Example asynchronous operation using the generic async initiation pattern.
  template <typename CompletionToken>
  auto async_write_some(const char* data,
                        std::size_t size,
                        CompletionToken&& token)
  {
    // The async result type deduces the handler signature.
    using handler_type = typename asio::async_result<CompletionToken,
                         void(asio::error_code, std::size_t)>::completion_handler_type;

    // Initiate the operation on the executor associated with the object.
    return asio::async_initiate<CompletionToken,
                                 void(asio::error_code, std::size_t)>(
        [&](handler_type&& handler)
        {
          // Here we could post the operation to the executor.
          this->get_executor().post(
            [this, data, size, h = std::move(handler)]() mutable
            {
              const implementation_type& impl = this->get_implementation();
              ssize_t bytes = ::write(impl.native_handle, data, size);
              if (bytes < 0)
                h(asio::error_code(errno, asio::error::get_system_category()), 0);
              else
                h(asio::error_code{}, static_cast<std::size_t>(bytes));
            });
        },
        std::forward<CompletionToken>(token));
  }
};

} // namespace my_namespace

The service_type typedef is automatically provided by basic_io_object, exposing your service's implementation_type to the wrapper class.

Complete Usage Example

Integrate your custom I/O object with standard ASIO patterns:

#include <asio.hpp>
#include "my_custom_io.hpp"

int main()
{
  asio::io_context ctx;

  // Construct the custom object.
  my_namespace::my_custom_io my_io(ctx);

  // Synchronous write.
  const char* msg = "Hello ASIO\n";
  my_io.write_some(msg, std::strlen(msg));

  // Asynchronous write with a lambda handler.
  my_io.async_write_some(msg, std::strlen(msg),
      [&](asio::error_code ec, std::size_t bytes)
      {
        if (!ec)
          std::printf("Async write succeeded, %zu bytes\n", bytes);
        else
          std::printf("Async write error: %s\n", ec.message().c_str());
      });

  ctx.run();          // Drive the asynchronous operation.
}

This example demonstrates automatic resource management—the service's construct and destroy methods handle the file descriptor lifecycle, while the async operation integrates with the io_context executor.

Summary

  • Custom ASIO I/O objects require a service class defining implementation_type, construct(), and destroy(), plus a wrapper class inheriting from asio::basic_io_object<YourService>.
  • The service pattern in include/asio/basic_io_object.hpp handles resource lifetime and move semantics automatically.
  • Synchronous operations access native handles through get_implementation().
  • Asynchronous operations use asio::async_initiate to support any completion token type (lambdas, futures, coroutines).
  • Reference implementations in include/asio/basic_socket.hpp demonstrate production-quality service design.

Frequently Asked Questions

What is the minimum set of methods required for a custom ASIO service?

A valid ASIO service must define using implementation_type = ...; to specify the native resource type, plus void construct(implementation_type&) to initialize resources and void destroy(implementation_type&) to clean them up. You must also provide a constructor accepting asio::io_context& and implement get_io_context() to return the stored context reference.

How does move semantics work in custom ASIO I/O objects?

Move semantics are optional but recommended. When you implement move_construct() and move_assign() in your service, ASIO detects these methods (via the service_has_move trait in basic_io_object.hpp) and enables move operations for the I/O object. The base class calls your service's move methods when the I/O object is moved, allowing you to transfer native handles between instances.

Can custom ASIO I/O objects be used with coroutines and co_spawn?

Yes. By implementing asynchronous operations using asio::async_initiate, your custom I/O object automatically supports any ASIO completion token, including coroutines. The async_result template deduces the appropriate handler type, allowing you to use co_spawn with asio::use_awaitable or other awaitable types.

Where can I find reference implementations in the ASIO source code?

Study include/asio/basic_socket.hpp for a complete I/O object implementation and include/asio/basic_io_object.hpp for the base class mechanics. These files demonstrate how built-in objects like sockets and timers structure their service relationships, showing production patterns for construction, destruction, and asynchronous operation initiation.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →