Files
GacUI/.github/KnowledgeBase/manual/vlppos/using-inter-process.md
T
2026-08-08 06:31:05 -07:00

23 KiB

Using Inter-Process Channels

The vl::inter_process namespace is organized in layers. Feature code normally uses the channel interfaces, because they describe typed named message delivery between connected participants. Transport code implements the raw network protocol interfaces, because they describe how one server and many clients exchange asynchronous WString messages. Portable async-socket transports and Windows-specific transports are composition choices that connect these two layers.

VlppOS provides the INetworkProtocol and IChannel interface families only for test-purpose local cross-process communication. Applications may use these public interfaces as integration contracts, but product code is strongly recommended to provide its own implementations instead of treating the bundled transports and channel stack as production networking.

Despite the names, these contracts are not tied to TCP/IP. Any mechanism that provides the required asynchronous message and lifecycle behavior can fit behind them, including stdio, DLL calls, shared memory, WebAssembly host calls, pipes, sockets, HTTP or a custom network protocol.

Keep this boundary in application code:

  • Use IChannelServer<TPackage>, IChannelClient<TPackage>, IChannel<TPackage> and IChannelReader<TPackage> when the feature sends and receives packages.
  • Use INetworkProtocolServer, INetworkProtocolClient, INetworkProtocolConnection and INetworkProtocolCallback when adding a new pipe, socket, HTTP, shared-memory or other underlying transport.
  • Use NetworkProtocolChannelServer<TPackage, TSerialization, TServerBase> and NetworkProtocolChannelClient<TPackage, TSerialization> when a channel should run on top of a raw protocol implementation.

Channel Interfaces for Features

A channel client is a participant with a server-assigned positive client id and a set of named channels. A channel is not a socket and it is not owned by the server alone. It is a logical lane shared by every connected participant that declares the same channel name.

The channel interfaces are the stable API for feature code:

  • IChannelReader<TPackage>::OnRead receives the sender client id and one typed package.
  • IChannel<TPackage> exposes Initialize, SendToClient, BroadcastFromClient and BatchWrite.
  • IChannelClient<TPackage> exposes channel names, generated channel objects, client status, WaitForServer and lifecycle callbacks.
  • IChannelServer<TPackage> accepts or rejects clients, records client ids, supports in-process local clients and broadcasts fatal channel errors.

Channel names are user-defined strings. A user channel name must not be empty and must not contain !. Names beginning with !, including !Error and !System, are reserved for the implementation.

Initialize installs one reader and replays messages that arrived before the reader was installed. There is no reader uninstallation. SendToClient queues a direct message to one client id. BroadcastFromClient queues a message for all other participants on the same channel, optionally excluding selected receiver ids. BatchWrite flushes queued messages and reports disconnection through its output argument.

IChannelServer<TPackage> manages delivery and bookkeeping. It is not itself a channel speaker. When server-side logic must send normal channel messages, create a NetworkProtocolLocalChannelClient<TPackage, TSerialization>, connect it with ConnectLocalClient, and send through that local client's channel objects. This keeps every normal channel message tied to a real positive sender client id.

A small channel client follows this shape:

using namespace vl;
using namespace vl::collections;
using namespace vl::inter_process;

constexpr const wchar_t* ChatChannelName = L"Chat";

struct WStringListSerializer
{
    using SourceType = List<WString>;
    using DestType = WString;
    using ContextType = std::nullptr_t;

    static void Serialize(const ContextType&, const SourceType& source, DestType& dest)
    {
        dest = WString::Empty;
        for (auto&& item : source)
        {
            dest += itow(item.Length()) + L":" + item;
        }
    }

    static void Deserialize(const ContextType&, const DestType& dest, SourceType& source)
    {
        source.Clear();
        const wchar_t* reading = dest.Buffer();
        const wchar_t* end = reading + dest.Length();
        while (reading < end)
        {
            auto delimiter = wcschr(reading, L':');
            CHECK_ERROR(delimiter && delimiter < end, L"Invalid package.");

            auto length = wtoi(WString::CopyFrom(reading, (vint)(delimiter - reading)));
            CHECK_ERROR(length >= 0 && delimiter + 1 + length <= end, L"Invalid package length.");

            source.Add(WString::CopyFrom(delimiter + 1, length));
            reading = delimiter + 1 + length;
        }
    }
};

using ChatChannelClientBase =
    NetworkProtocolChannelClient<WString, WStringListSerializer>;

class ChatClient
    : public ChatChannelClientBase
    , public virtual IChannelReader<WString>
{
private:
    IChannelClient<WString>::ChannelMap channelNames;
    IChannel<WString>* chat = nullptr;

public:
    ChatClient(Ptr<INetworkProtocolClient> transport)
        : ChatChannelClientBase(transport)
    {
        channelNames.Add(ChatChannelName, nullptr);
        auto&& channels = GetChannels();
        auto index = channels.Keys().IndexOf(ChatChannelName);
        CHECK_ERROR(index != -1, L"Missing chat channel.");
        chat = channels.Values()[index];
        chat->Initialize(this);
    }

    const IChannelClient<WString>::ChannelNameList& OnGetChannelNames() override
    {
        return channelNames.Keys();
    }

    void OnConnected(vint clientId) override
    {
        // Record the assigned positive client id if the feature needs it.
    }

    void OnRead(vint senderClientId, const WString& package) override
    {
        // Handle one typed package from senderClientId.
    }

    void Send(vint receiverClientId, const WString& package)
    {
        bool disconnected = false;
        chat->SendToClient(receiverClientId, package);
        chat->BatchWrite(disconnected);
        if (disconnected)
        {
            // Stop using this client.
        }
    }
};

Network Protocol Interfaces for Transports

The raw protocol layer is the extension point for a new underlying implementation. It intentionally knows nothing about channel names or package types. One INetworkProtocolConnection exchanges asynchronous WString messages with the other side. This message contract can be implemented by a local pipe or socket, stdio, DLL calls, shared memory, a WebAssembly host bridge or another mechanism; the name INetworkProtocol does not require an IP network.

Implement these interfaces for a new transport:

  • INetworkProtocolConnection: install or uninstall one callback, start the asynchronous read loop, send one string message and stop the connection.
  • INetworkProtocolCallback: receive strings, remote errors, local errors, connected and disconnected events. Callback methods may run on any thread, so implementations and users must be thread-safe.
  • INetworkProtocolClient: own one connection, block in WaitForServer until the connection is established or fails, and report ClientStatus.
  • INetworkProtocolServer: start accepting, call OnClientConnected for accepted transport connections, stop all owned connections and report stopped state.

InstallCallback should call OnInstalled with the connection. Passing nullptr uninstalls the callback. BeginReadingLoopUnsafe starts receiving messages asynchronously, but callers must tolerate implementations that have already received data after the callback was installed. Stop is the shutdown boundary; after it returns, pending transport callbacks should no longer touch the stopped object.

A raw client must return a connection object from GetConnection, but using that connection before WaitForServer finishes is outside the contract. A raw server should not report client-connected callbacks before Start or after Stop. If a connection is lost, the same connection object does not reconnect; a reconnecting transport should create a new connection object.

After a raw transport exists, reuse the channel bridge instead of reimplementing channel routing:

class MyConnection : public Object, public virtual INetworkProtocolConnection
{
    // Store one callback, frame transport bytes into WString messages,
    // call OnReadString / OnLocalError / OnDisconnected, and drain callbacks in Stop.
};

class MyClient : public Object, public virtual INetworkProtocolClient
{
    // Own one MyConnection and implement WaitForServer / GetStatus.
};

class MyServer : public Object, public virtual INetworkProtocolServer
{
    // Accept MyConnection objects and pass them to OnClientConnected.
};

using MyChannelServer =
    NetworkProtocolChannelServer<MyPackage, MyPackageListSerializer, MyServer>;

using MyChannelClient =
    NetworkProtocolChannelClient<MyPackage, MyPackageListSerializer>;

Default Channel Bridge

NetworkProtocolChannelServer, NetworkProtocolChannelClient and NetworkProtocolLocalChannelClient translate channel batches into raw WString protocol messages. The bridge requires a serializer where:

  • TSerialization::SourceType is collections::List<TPackage>.
  • TSerialization::DestType is WString.
  • TSerialization::ContextType stores optional serializer state, or std::nullptr_t when no state is required.
  • Serialize converts a package list to one WString.
  • Deserialize converts one WString back to a package list.

The bridge uses NetworkPackage as the raw text envelope. Its string shape is clientId,extraClientId1,...;channelName;messageBody. For client-to-server messages, clientId is the direct receiver id; an empty id means broadcast, and extra ids are blocked broadcast receivers. For server-to-client messages, clientId is the positive sender id.

A network channel client connects by sending one handshake package with an empty client id, an empty channel name and all supported channel names joined by !. The server validates the names, assigns a positive client id, records channel membership and sends the id back in an empty-channel response. After that, normal channel packages are delivered only when sender and receiver membership matches the channel name.

BroadcastError is a terminal admission boundary. The first broadcast error is retained, later broadcasts are ignored, and new network or local admissions are rejected. If an application OnClientConnected callback was already running, the server keeps the underlying transport alive until an accepted client receives that retained error and disconnects. A committed client is not included in the broadcast snapshot until its client-id response or local OnConnected callback completes, preserving connected-before-fatal ordering. Concurrent Stop calls share this barrier and cannot stop the transport early. Callback-reentrant terminal calls never wait on themselves; when broadcast owns the terminal boundary, physical stop is deferred until protected raw protocol, admission, fatal-delivery and disconnection callbacks unwind. If the last barrier is a raw protocol callback, completion moves to another thread so the underlying transport can drain that callback. A non-reentrant Stop reports a recorded shutdown exception once; a most-derived server destructor must call Stop before destroying callback-visible fields and catch any exception.

A channel server over a Windows named pipe can be declared like this:

#include <vlppos.h>
#include <VlppOS.Windows.h>

using namespace vl;
using namespace vl::inter_process;

class ChatServer
    : public NetworkProtocolChannelServer<WString, WStringListSerializer, named_pipe::NamedPipeServer>
{
public:
    ChatServer(const WString& pipeName)
        : NetworkProtocolChannelServer<WString, WStringListSerializer, named_pipe::NamedPipeServer>(pipeName)
    {
    }

    WaitForClientResult OnClientConnected(
        vint clientId,
        const IChannelClient<WString>::ChannelNameList& availableChannels,
        IChannelClient<WString>* localClient) override
    {
        return availableChannels.Contains(ChatChannelName)
            ? WaitForClientResult::Accept
            : WaitForClientResult::Reject;
    }
};

auto server = Ptr(new ChatServer(L"ExamplePipe"));
server->Start();

Portable Async-Socket Implementations

The types in vl::inter_process::async_tcp_socket form a portable, loopback-only stack for asynchronous TCP bytes, HTTP/1.1 requests and Mini HTTP services. Platform-neutral factories select windows_socket::AsyncSocketServer and AsyncSocketClient, linux_socket::AsyncSocketServer and AsyncSocketClient, or macos_socket::AsyncSocketServer and AsyncSocketClient at the application composition boundary.

Direct Length-Framed Transport

Use async_tcp_socket::NetworkProtocolServer<TAsyncSocketServer> and async_tcp_socket::NetworkProtocolClient<TAsyncSocketClient> when both peers use VlppOS and HTTP compatibility is unnecessary. One logical connection is one physical TCP socket. Each WString is framed with its character count and native wchar_t bytes, so this is not a portable wire encoding between peers with different wchar_t representations.

These templates construct their socket backend from forwarded constructor arguments, so choose the concrete type for the target: windows_socket::AsyncSocketServer and AsyncSocketClient, linux_socket::AsyncSocketServer and AsyncSocketClient, or macos_socket::AsyncSocketServer and AsyncSocketClient, and pass the port to the protocol adapter. They do not accept an injected Ptr<IAsyncSocketServer> or Ptr<IAsyncSocketClient>.

IAsyncSocketServer, IAsyncSocketClient, IAsyncSocketConnection and IAsyncSocketCallback expose the underlying ordered full-duplex byte streams. The interface-returning CreateDefaultAsyncSocketServer(port) and CreateDefaultAsyncSocketClient(port) factories are for code that consumes these socket interfaces, including the injected SocketHttpServer and SocketHttpClient dependencies below. Each socket exposes its immutable loopback port through GetPort, and IAsyncSocketClient::CreateSameEndpointClient creates a fresh client for another physical lane on the same endpoint.

HTTP-Compatible Socket Transport

Use async_tcp_socket::SocketHttpServer and async_tcp_socket::SocketHttpClient when the transport must use the VlppOS HTTP wire protocol or interoperate with the Windows HTTP implementation. They implement INetworkProtocolServer, INetworkProtocolClient and INetworkProtocolConnection over the portable Mini HTTP stack.

The public construction surface keeps native socket composition explicit:

SocketHttpServer(
    Ptr<IAsyncSocketServer> socketServer,
    const WString& urlPrefix
    );

SocketHttpClient(
    Ptr<IAsyncSocketClient> socketClient,
    const WString& server,
    const WString& urlPrefix
    );

The server adapter uses the injected listener and never creates another one. The client uses the injected client for its first physical lane and calls CreateSameEndpointClient for the additional lanes required by full-duplex polling and recovery. The server name must select loopback, and the URL prefix is empty for the origin root or begins with /.

The adapter uses the same /VlppInterProcess/Connect, /VlppInterProcess/Request/{token} and /VlppInterProcess/Response/{token} routes as windows_http::HttpServer and windows_http::HttpClient. A successful nonempty Request response is provisional delivery: the next Request for the same token is its implicit acknowledgement and must arrive within five seconds. The deadline starts only when the server actually sends a message, so an idle connection has no heartbeat deadline. If the response fails, the server restores the message to the FIFO head; if the acknowledgement is missing, it reports a nonfatal local error. A raw callback may decline promotion, while an admitted NetworkProtocolChannelServer connection promotes either error, disconnects, unregisters the token and rejects any late Request carrying that stale token. The protocol still has no deduplication, heartbeat or explicit disconnect route and must not be treated as exactly-once delivery.

Portable Mini HTTP Request Helpers

async_tcp_socket::HttpRequest, HttpResponse, HttpRequestServer, HttpRequestClient and HttpRequestConnection provide the lower HTTP/1.1 message layer. Use them when code needs complete binary-safe request and response objects and controls each connection directly.

async_tcp_socket::SocketHttpServerApi, SocketHttpRequestContext and SocketHttpClientApi add prefix dispatch and request/response conveniences. A server API receives (socketServer, urlPrefix), and multiple APIs share one listener only when they receive the exact same server object. Override OnHttpRequestReceived, inspect the relative path, query or UTF-8 body, and complete the one-shot context with Respond, RespondStatus, RespondBytes, RespondUtf8 or Cancel. A client API receives (socketClient, server) and queues asynchronous HttpQuery calls.

SocketHttpClientApi uses the windows_http::HttpRequest, HttpResponse and HttpError value types, but those types are platform-neutral and using them here does not invoke WinHTTP. The TCP, HTTP and Mini HTTP implementations in this section are local test, validation and demonstration infrastructure rather than a production web stack.

Windows Implementations

Named pipes and the legacy HTTP.sys/WinHTTP transport are Windows-only. They are available for local tests and as reference implementations for the raw protocol contract, but do not make portable feature code depend on them or treat them as production communication stacks.

Named Pipe Transport

vl::inter_process::named_pipe::NamedPipeServer implements INetworkProtocolServer. vl::inter_process::named_pipe::NamedPipeClient derives from vl::inter_process::named_pipe::NamedPipeConnection and implements INetworkProtocolClient. vl::inter_process::named_pipe::NamedPipeConnection implements INetworkProtocolConnection.

The server starts overlapped named-pipe accepting in Start and owns both accepted connections and pending accepts. The client opens \.\pipe\NAME, waits for the server and switches the pipe into message-read mode. SendString frames one WString with length data. The implementation chunks writes because a Windows named pipe message is limited to 64K bytes. Broken-pipe cases become disconnection events, and fatal local pipe failures become OnLocalError followed by disconnection.

HTTP Transport

vl::inter_process::windows_http::HttpServer derives from vl::inter_process::windows_http::HttpServerApi and implements INetworkProtocolServer. vl::inter_process::windows_http::HttpClient implements both INetworkProtocolClient and INetworkProtocolConnection. This transport is a raw protocol implementation over Windows HTTP APIs, not a general web framework.

The raw HTTP protocol uses these routes under the configured base URL:

  • GET /VlppInterProcess/Connect creates a logical connection and returns per-connection request and response URLs.
  • POST /VlppInterProcess/Request/GUID is the client-maintained long-poll request for server-to-client messages.
  • POST /VlppInterProcess/Response/GUID sends client-to-server messages and may also receive one queued server-to-client message.

vl::inter_process::windows_http::HttpClient::WaitForServer sends the connect request, validates the returned URLs, records them and reports connection. BeginReadingLoopUnsafe starts the long-poll request loop. SendString posts to the response URL. Each recoverable failed Connect, Request or Response exchange calls INetworkProtocolCallback::OnLocalError with fatal == false before retrying; Connect and Response retry a limited number of times, while Request retries while the client is still running. Bounded retry exhaustion remains raw-fatal. Returning true from OnLocalError promotes a recoverable failure and makes the raw client stop after the callback returns. NetworkProtocolChannelClient does this for every local error after its channel reaches Connected, forwards fatal == true to its IChannelClient user and disconnects the channel; pre-handshake Connect retries remain under raw-client policy.

vl::inter_process::windows_http::HttpServer creates a vl::inter_process::windows_http::HttpServerConnection for each connect request. Server-to-client messages are returned through a pending long-poll request when possible, or queued until the next request. A successful message response starts a five-second acknowledgement deadline; the next Request for the same GUID cancels it. A failed response retains the message, while a missing acknowledgement reports a recoverable OnLocalError. An admitted channel promotes either error, stops the logical connection and unregisters the GUID, so a late Request is rejected as unknown. No deadline runs while the server is idle. Client-to-server request bodies are dispatched as inbound strings. When the server stops, pending long-poll requests are cancelled and connection callbacks receive disconnection.

Windows HTTP Helper APIs

vl::inter_process::windows_http::HttpClientApi and vl::inter_process::windows_http::HttpServerApi are lower-level Windows helpers used by vl::inter_process::windows_http::HttpClient and vl::inter_process::windows_http::HttpServer. Use them directly when a Windows feature needs asynchronous HTTP request and response behavior without adopting the raw INetworkProtocol transport shape.

vl::inter_process::windows_http::HttpClientApi owns one WinHTTP session and connection for a host and port. HttpQuery sends one asynchronous request described by vl::inter_process::windows_http::HttpRequest, whose fields include method, query, body, content type, accept types, credentials, cookies, extra headers and timeouts. Results are either vl::inter_process::windows_http::HttpResponse or vl::inter_process::windows_http::HttpError; HTTP status codes such as 404 are represented as HttpResponse values.

vl::inter_process::windows_http::HttpServerApi owns one HTTP.sys URL prefix. Override OnHttpRequestReceived to dispatch requests and OnHttpServerStopping to clean up feature state. Helpers such as GetUtf8Body, SendResponse and SendResponseUtf8 handle common request-body and response tasks.