From 794f057abaab689caf52f27d3c2f7e4bd0a42248 Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Wed, 13 May 2026 22:40:00 +0200 Subject: [PATCH 1/4] Fix the level of the See Also header in CEFBrowser class. --- docs/Reference/CEF/CefBrowser/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Reference/CEF/CefBrowser/index.md b/docs/Reference/CEF/CefBrowser/index.md index 8941f76..b36c081 100644 --- a/docs/Reference/CEF/CefBrowser/index.md +++ b/docs/Reference/CEF/CefBrowser/index.md @@ -551,7 +551,7 @@ Private Sub CefBrowser1_SourceChanged(ByVal IsNewDocument As Boolean) End Sub ``` -### See Also +## See Also - [CefEnvironmentOptions](EnvironmentOptions) -- pre-creation configuration carried by [**EnvironmentOptions**](#environmentoptions) - [CefLogSeverity](../Enumerations/CefLogSeverity), [cefPrintOrientation](../Enumerations/cefPrintOrientation) -- the package's two user-facing enumerations From 5396f773c8fb8e1f52b55aba85d86d940d7f0197 Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Wed, 13 May 2026 22:42:42 +0200 Subject: [PATCH 2/4] Update the WIP. --- WIP.md | 189 +++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 184 insertions(+), 5 deletions(-) diff --git a/WIP.md b/WIP.md index 754f786..9c90d12 100644 --- a/WIP.md +++ b/WIP.md @@ -4,7 +4,7 @@ Jekyll site (`just-the-docs` theme) deploying to `docs.twinbasic.com`. Source un ## Status -Initial reference documentation is **complete**. All eight packages have full reference coverage adapted from primary sources (Microsoft VBA-Docs CC-BY-4.0 for the runtime library, `.twin` source for the twinBASIC-specific packages); the CEF and WebView2 packages also carry a tutorial set. +Initial reference documentation is **complete**. All nine packages have full reference coverage adapted from primary sources (Microsoft VBA-Docs CC-BY-4.0 for the runtime library, `.twin` source for the twinBASIC-specific packages); the CEF and WebView2 packages also carry a tutorial set. | Package | Reference | Tutorials | |--------------------------------------|-----------|-----------| @@ -16,6 +16,7 @@ Initial reference documentation is **complete**. All eight packages have full re | CustomControls / CustomControlsPackage | done | — | | cefPackage (CEF) | done | done | | WinEventLogLib | done | — | +| WinNamedPipesLib | done | — | The rest of this file is the maintenance guide for adding new pages or updating existing ones — primary-source paths, page templates, cross-section linking conventions, the per-symbol workflow, and the integrity check. @@ -32,6 +33,7 @@ When working from a primary source: always read it first — **never paraphrase - `docs/Reference/CustomControls/` — CustomControls package: the eight **Waynes…** custom controls, their shared `Styles/` helper classes (`Fill`, `Borders`, `Corners`, `TextRendering`, …), the `Framework/` DESIGNER surface (interfaces, CoClasses, the `Canvas` / `SerializeInfo` UDTs), and the `Enumerations/` (`CornerShape`, `FillPattern`, `DockMode`, …). - `docs/Reference/CEF/` — CEF (Chromium Embedded Framework) package: the **CefBrowser** control, its `EnvironmentOptions` sub-page, and the two user-facing enumerations (`CefLogSeverity`, `cefPrintOrientation`). This is a much smaller surface than WebView2 — the package is currently BETA and many WebView2-equivalent features are not yet exposed. - `docs/Reference/WinEventLogLib/` — Windows Event Log package: the generic `EventLog(Of T1, T2)` class and the `EventLogHelperPublic` module with its single `RegisterEventLogInternal` helper. Three pages total — `index.md`, `EventLog.md`, `EventLogHelperPublic.md`. +- `docs/Reference/WinNamedPipesLib/` — Windows Named Pipes package: the IOCP-based async pipe framework — `NamedPipeServer` + `NamedPipeServerConnection` on the server side, `NamedPipeClientManager` + `NamedPipeClientConnection` on the client side. Five pages total (`index.md` + one per class). - `docs/Reference/Statements.md` — alphabetical index of language statements. - `docs/Reference/Procedures and Functions.md` — alphabetical index of procedures/functions. - `docs/_includes/footer_custom.html` — overrides the theme's footer slot; renders the copyright line and, when `vba_attribution: true` is set in a page's frontmatter, an additional CC-BY-4.0 attribution line beneath it. @@ -64,6 +66,7 @@ All of twinbasic's package sources are at: ..\tb-export\NewProject\Packages\CustomControlsPackage\Sources\ ..\tb-export\NewProject\Packages\cefPackages\Sources\ ..\tb-export\NewProject\Packages\WinEventLogLib\Sources\ +..\tb-export\NewProject\Packages\WinNamedPipesLib\Sources\ etc. ``` @@ -451,6 +454,160 @@ Class-level decoration on `EventLog`: `[COMCreatable(False)]`, `[ClassId("4AEA12 **License:** MIT (copyright Wayne Phillips T/A iTech Masters, 2025; first release v0.1, 04-FEB-2025) — same situation as WebView2Package, Assert, CustomControls, and CEF. Pages are fully original content; **omit** the `vba_attribution: true` flag. +### WinNamedPipesLib + +Layout of `..\tb-export\NewProject\Packages\WinNamedPipesLib\Sources\` is flat — seven `.twin` files plus three text files (`_README.txt`, `_LICENCE.txt`, `_RELEASE_HISTORY.txt`): + +- `APIs.twin` — `Private Module NamedPipesAPIs` wrapping Win32 declarations from `kernel32.dll`, `user32.dll`, and `oleaut32.dll` (`CreateNamedPipeW`, `ConnectNamedPipe`, `ReadFile` / `WriteFile`, `CreateIoCompletionPort`, `GetQueuedCompletionStatus`, `PostQueuedCompletionStatus`, `CreateThread`, …) plus the supporting `Type` declarations (`POINT`, `MSG`, `SAFEARRAYBOUND`, `SAFEARRAY_1D`, `OVERLAPPED_CUSTOM`, `FILETIME`, `WIN32_FIND_DATAW`) and a 32/64-bit `SetWindowLongPtrW` alias. No doc page. +- `Constants.twin` — `Private Module NamedPipesConstants` carrying Win32 constants (`PIPE_ACCESS_DUPLEX`, `PIPE_TYPE_MESSAGE`, `FILE_FLAG_OVERLAPPED`, `ERROR_IO_PENDING`, `ERROR_MORE_DATA`, …) and the package-internal `Enum OverlappedTypeConstants` (`tbOverlappedConnect`, `tbOverlappedRead`, `tbOverlappedWrite`). Module is private, so none of these surface in the public API; no doc page. +- `Helper.twin` — `Private Module NamedPipesHelper` with `ObjectFromPointer_*` and `ObjPtrRef` / `ObjReleaseRef` reference-counting helpers used internally to ferry COM pointers across IOCP worker threads. Module is private; no doc page. +- `NamedPipeServer.twin` — `Public Class NamedPipeServer`. The server-side entry point. Also declares the private `INamedPipeServerInternal` interface (implementation detail; no doc page). +- `NamedPipeServerConnection.twin` — `Public Class NamedPipeServerConnection`. Per-client connection object surfaced through `NamedPipeServer` events. Also declares the private `INamedPipeServerConnectionInternal` interface (no doc page). +- `NamedPipeClientManager.twin` — `Public Class NamedPipeClientManager`. Owns the client-side IOCP machinery and the `Connect` / `Stop` / `FindNamedPipes` entry points. Also declares the private `INamedPipeClientManagerInternal` interface (no doc page). +- `NamedPipeClientConnection.twin` — `Public Class NamedPipeClientConnection`. Returned by `NamedPipeClientManager.Connect`. Carries the `Connected` / `Disconnected` / `MessageReceived` / `MessageSent` events and the `AsyncWrite` / `AsyncRead` / `AsyncClose` methods. Also declares the private `INamedPipeClientConnectionInternal` interface (no doc page). + +The four classes are each tagged `[COMCreatable(False)]` — only the manager / server classes can be instantiated by user code (with `New`); the two `Connection` classes are constructed internally and handed back through events / return values. + +The private `INamedPipe*Internal` interfaces serve a marshalling-only role: each public class implements its matching internal interface so that the IOCP worker threads can refcount and dispatch through `stdole.IUnknown` without taking a strong reference to the parent class. They are *not* user-facing surface and get no documentation page. + +Public user-facing surface (four classes — two on each side): + +| Class | Role | +|-----------------------------|---------------------------------------------------------------------------------------------------------------| +| `NamedPipeServer` | The server. User-instantiated. Sets `PipeName`, calls `Start`, listens for events; one server hosts many clients. | +| `NamedPipeServerConnection` | One server-side per-client connection. Surfaced through `NamedPipeServer` events; carries `AsyncRead` / `AsyncWrite` / `AsyncClose`. | +| `NamedPipeClientManager` | The client-side coordinator. User-instantiated. Owns the IOCP worker threads; the `Connect` method returns a `NamedPipeClientConnection`. | +| `NamedPipeClientConnection` | One client-side connection. Returned by `NamedPipeClientManager.Connect`; carries `Connected` / `Disconnected` / `MessageReceived` / `MessageSent` events and `AsyncRead` / `AsyncWrite` / `AsyncClose`. | + +#### `NamedPipeServer` public members + +Tagged `[COMCreatable(False)]`, `[InterfaceId(...)]`, `[EventInterfaceId(...)]`, `[ClassId(...)]`. No `[Description("...")]` on the class itself. + +**Public fields** (each carries a `[Description("...")]`): + +- `PipeName As String` — *"the discoverable pipe name"*. Set this before `Start()` or `Start()` raises run-time error 5 (*"cannot start without specifying a pipe name"*). The Win32 pipe namespace path is `\\.\pipe\` (the package prepends `\\.\pipe\` itself). +- `NumThreadsIOCP As Long = 1` — *"the number of IOCP worker threads that will be created"*. Read once when `Start()` is called; the in-source `FIXME` notes that this should become read-only once started. +- `FreeThreadingEvents As Boolean = False` — *"set to TRUE to allow the server events ClientConnected / ClientReceivedDataAsync etc to be fired directly from the IOCP worker threads. set to FALSE to ensure the events get fired on the main UI thread."* The free-threaded path skips a Win32 message-loop round-trip; the marshalled path is safer because the events fire on the UI thread. +- `ContinuouslyReadFromPipe As Boolean = True` — *"set to TRUE to ensure ClientReceivedDataAsync events always fire without having to call AsyncRead manually."* When `False`, the consumer must call `AsyncRead` after each `ClientMessageReceived` to keep receiving. +- `MessageBufferSize As Long = 131072` — *"sets the initial size for ReadFile() buffers. does not affect the maximum message receive size, but can affect performance."* On `ERROR_MORE_DATA` the IOCP loop allocates a larger overflow buffer and re-issues the read, so messages larger than this size do work — but with one extra allocation per overflowed message. + +**Public events**: + +- `ServerReady()` — fires once after `Start()` when every IOCP worker has joined. +- `ClientConnected(Connection As NamedPipeServerConnection)` — a new client connection has completed. +- `ClientDisconnected(Connection As NamedPipeServerConnection)` — the connection has dropped and every outstanding async operation has returned. +- `ClientMessageReceived(Connection As NamedPipeServerConnection, ByRef Cookie As Variant, ByRef Data() As Byte)` — a message arrived. *Data* is a transient view over the IOCP read buffer (a hand-rolled `SAFEARRAY` whose backing memory is reused after the event); copy it if you need to keep it past the event handler. +- `ClientMessageSent(Connection As NamedPipeServerConnection, ByRef Cookie As Variant)` — a previously-issued `AsyncWrite` has completed. + +**Public methods**: + +- `Sub New()` — constructor; creates the hidden marshalling-window used for UI-thread event delivery. +- `Public Sub Start()` — creates the IOCP completion port and `NumThreadsIOCP` worker threads, then issues the first connection listener. Idempotent: calling `Start()` while already started is a no-op. +- `Public Sub Stop()` — cancels every outstanding I/O, joins the IOCP threads, closes pipe handles. Idempotent. Called automatically from `Class_Terminate`. +- `Sub AsyncBroadcast(ByRef Data() As Byte, Optional ByRef Cookie As Variant = Empty)` — issues `AsyncWrite` against every currently-connected `NamedPipeServerConnection`. +- `Public Sub ManualMessageLoopEnter()` / `Public Sub ManualMessageLoopLeave()` — drive a Win32 message loop manually (rare; only needed when the host process does not naturally pump messages — e.g. an unattended Windows service that wants the marshalled-event semantics rather than the free-threaded ones). `Leave` posts `WM_USER_QUITTING`, which `Enter` reads to break the loop. + +#### `NamedPipeServerConnection` public members + +Tagged `[COMCreatable(False)]`. Not directly user-instantiable. + +**Public fields**: + +- `Handle As LongPtr` — the underlying Win32 pipe handle. Exposed but not normally needed; useful for low-level operations or debugging. +- `IsOpening As Boolean` — true while `Open()` is in progress (race-condition window between adding to the linked list and finishing `ConnectNamedPipe`). +- `IsConnected As Boolean` — true between the client connecting and the connection dropping. +- `CustomData As Variant` — *"free for use"*: opaque per-connection slot the consumer can attach state to. + +**Public methods**: + +- `Sub New(...)` — internal constructor; takes the parent server + pipe info. Never called by user code. +- `Public Sub AsyncClose()` — cancels outstanding I/O and closes the pipe handle. Called automatically from `Class_Terminate`. +- `Public Sub AsyncWrite(ByRef Data() As Byte, Optional ByRef Cookie As Variant = Empty)` — writes a message back to this specific client. Returns immediately; `NamedPipeServer.ClientMessageSent` fires when the write completes. +- `Public Sub AsyncRead(Optional ByRef Cookie As Variant = Empty, Optional OverlappedStruct As LongPtr)` — manually issues a read. Only needed when `NamedPipeServer.ContinuouslyReadFromPipe = False`; otherwise the server keeps the read pump fed automatically. + +No public events — message-received and connection-dropped notifications come through the parent `NamedPipeServer`. The class declares an internal `INamedPipeServerConnectionInternal` interface that the IOCP loop uses for refcounting; that interface is `Private` and gets no doc page. + +#### `NamedPipeClientManager` public members + +Tagged `[COMCreatable(False)]`, `[InterfaceId(...)]`, `[EventInterfaceId(...)]`, `[ClassId(...)]`. + +**Public fields** (each carries `[Description("...")]`, mirror the server's): + +- `NumThreadsIOCP As Long = 1` +- `MessageBufferSize As Long = 131072` +- `FreeThreadingEvents As Boolean = False` +- `ContinuouslyReadFromPipe As Boolean = True` + +These four are read once on the first `Connect()` call and propagated to every `NamedPipeClientConnection` created through that manager; subsequent changes do not affect connections that already exist. Source comment in `NamedPipeClientConnection` confirms: *"tip: set it in NamedPipeClientConnections before Connect()"*. + +**Public methods**: + +- `Sub New()` — constructor; creates the hidden marshalling window. +- `Public Function Connect(ByVal PipeName As String) As NamedPipeClientConnection` — opens a connection to a server (`\\.\pipe\`). Lazy on first call: creates the IOCP port and the worker threads. Returns a connection object that fires `Connected` once the async `CreateFileW` completes. +- `Public Sub Stop()` — cancels every outstanding I/O on every managed connection, joins the IOCP threads, frees the resources. Idempotent. Called automatically from `Class_Terminate`. +- `Public Function FindNamedPipes(Optional Pattern As String = "*") As Collection` — enumerates the named pipes currently published on the local machine (via `FindFirstFileW("\\.\pipe\")`). Returns a `Collection` of `String`. Useful as a discovery helper before calling `Connect`. + +No events on the manager itself — per-connection events live on the returned `NamedPipeClientConnection` objects. + +#### `NamedPipeClientConnection` public members + +Tagged `[COMCreatable(False)]`, `[InterfaceId(...)]`, `[ClassId(...)]`, `[EventInterfaceId(...)]`. Not directly user-instantiable — `NamedPipeClientManager.Connect` returns it. + +**Public fields**: + +- `PipeName As String` — the pipe name the connection targets. +- `Handle As LongPtr` — the underlying Win32 file handle. Same caveats as on the server-side connection. +- `CustomData As Variant` — *"free for use"*. + +**Public events**: + +- `Connected()` — the async `CreateFileW` has succeeded. +- `Disconnected()` — the connection has dropped and every outstanding async operation has returned. +- `MessageReceived(ByRef Cookie As Variant, ByRef Data() As Byte)` — a message arrived. *Data* has the same transient-view semantics as on the server. +- `MessageSent(ByRef Cookie As Variant)` — a previously-issued `AsyncWrite` has completed. + +**Public methods**: + +- `Sub New(...)` — internal constructor; never called by user code directly. +- `Public Sub AsyncClose()` — **critical:** the README says *"you MUST call AsyncClose on the client side, otherwise the connection is left alive when the object goes out of scope"*. Surface this on every relevant page. +- `Public Sub AsyncWrite(ByRef Data() As Byte, Optional ByRef Cookie As Variant = Empty)` — sends a message to the server. +- `Public Sub AsyncRead(Optional ByRef Cookie As Variant = Empty, Optional OverlappedStruct As LongPtr)` — manually issues a read. Same gating as on the server-side: only call this when `ContinuouslyReadFromPipe = False`. + +**Documented gaps / TODOs from `_README.txt`** (surface on the landing page): + +- *"we need a method to allow closing a client connection from the server side"* — there is no `NamedPipeServerConnection.Disconnect` or `.Close` user-method today. The server can stop the whole pipe (`NamedPipeServer.Stop`) but cannot selectively drop one client. +- *"named pipe error should be raised via Error events (rather than throwing an error on the worker threads)"* — internal IOCP errors currently bubble up as VBA run-time errors on worker threads rather than as `Error` events. No `Error` event exists on any of the four classes yet. +- *"remove max size 131072 of messages"* — the `MessageBufferSize` initial-buffer default is 131072 bytes. The IOCP overflow path (`ERROR_MORE_DATA` → larger buffer → re-issue read) does handle larger messages, but there may be a hard cap somewhere the author wants to remove; surface this as *"see TODO list in `_README.txt`"* rather than making a stronger claim. +- *"currently a lot of duplicate code in server + client"* — internal-refactor note. **Not** surfaced on the docs. + +**Cookie pattern.** Every `AsyncRead` and `AsyncWrite` accepts an optional *Cookie* (`Variant`). Whatever the consumer passes in flows through the IOCP completion buffer and is handed back out on the matching `MessageReceived` / `MessageSent` event. This is the package's mechanism for correlating individual writes with their completion notifications when many are in flight. + +**`Data() As Byte` transience.** Inside `MessageReceived` / `ClientMessageReceived`, *Data* is **not** a real `Byte` array — it is a hand-rolled `SAFEARRAY` whose `pvData` field points at the IOCP overlapped buffer. The buffer is recycled back into a free-list at the end of the event handler. Copy the bytes out (`ReDim`-and-copy, or `CStrConv` for text payloads) if you need them after returning from the handler. The source uses `PutMemPtr(VarPtr(safeArrayPtr), VarPtr(safeArrayPsuedo))` and clears it afterwards — surface this lifetime caveat on every event-page entry that carries *Data*. + +**Hidden message window.** Each `NamedPipeServer` and `NamedPipeClientManager` instance creates an invisible `STATIC`-class window with a subclassed `WndProc`, used to marshal IOCP-thread completions back to the UI thread when `FreeThreadingEvents = False`. Mention this on each class's intro paragraph — it explains why the consumer's process must be pumping a message loop for the default event-delivery semantics to work, and why `ManualMessageLoopEnter` / `ManualMessageLoopLeave` exist on `NamedPipeServer` for service / console hosts. + +**Layout decision** — five pages total, one per public class plus the landing page: + +``` +docs/Reference/WinNamedPipesLib/ + index.md ← package landing; intro + IOCP model + cookie + transient-data caveat + gap list + class table + NamedPipeServer.md ← single-file: fields, events, methods + NamedPipeServerConnection.md ← single-file + NamedPipeClientManager.md ← single-file + NamedPipeClientConnection.md ← single-file +``` + +All four class pages are single-file (no folder-style — no natural sub-pages; the surface per class is medium-small, on the order of WebView2 wrapper classes). + +**Naming:** + +- Folder / URL segment: `WinNamedPipesLib/` (matches the source-side package name; no `Package` suffix to drop, same as `WinEventLogLib`). +- Index title: `WinNamedPipesLib Package` — the ` Package` convention. +- Permalinks: `/tB/Packages/WinNamedPipesLib/` for the landing; `/tB/Packages/WinNamedPipesLib/` for each of the four class pages. +- `parent: WinNamedPipesLib Package` on each child page (matching the index `title:`). + +**License:** MIT (copyright Wayne Phillips T/A iTech Masters, 2025; first release v0.1, 04-FEB-2025) — same situation as WebView2Package, Assert, CustomControls, CEF, and WinEventLogLib. Pages are fully original content; **omit** the `vba_attribution: true` flag. + ## Page template Match the existing style. Worked examples to imitate: @@ -535,6 +692,7 @@ The URL prefixes are *not* uniform across packages — VBA pages live one segmen - CEF enumeration → `/tB/Packages/CEF/Enumerations/` - WinEventLogLib class → `/tB/Packages/WinEventLogLib/EventLog` (single-file; same depth as a single-file VB class) - WinEventLogLib module → `/tB/Packages/WinEventLogLib/EventLogHelperPublic` (single-file; same depth as an Assert module) +- WinNamedPipesLib class → `/tB/Packages/WinNamedPipesLib/` (single-file; same depth as a single-file VB class) Common patterns: @@ -605,6 +763,9 @@ Common patterns: | WinEventLogLib `Packages/WinEventLogLib/X` | sibling `Packages/WinEventLogLib/Y` | `[Y](Y)` | | WinEventLogLib `Packages/WinEventLogLib/X` | VBA `Modules//Y` | `[Y](../../Modules//Y)` | | WinEventLogLib `Packages/WinEventLogLib/X` | `Core/Y` | `[Y](../../Core/Y)` | +| WinNamedPipesLib `Packages/WinNamedPipesLib/X` | sibling `Packages/WinNamedPipesLib/Y` | `[Y](Y)` | +| WinNamedPipesLib `Packages/WinNamedPipesLib/X` | VBA `Modules//Y` | `[Y](../../Modules//Y)` | +| WinNamedPipesLib `Packages/WinNamedPipesLib/X` | `Core/Y` | `[Y](../../Core/Y)` | | `Core/X` | VBA `Modules//Y` | `[Y](../Modules//Y)` | | `Core/X` | VBRUN `Packages/VBRUN//Y` | `[Y](../Packages/VBRUN//Y)` | | `Core/X` | VB `Packages/VB/Y` | `[Y](../Packages/VB/Y)` | @@ -613,6 +774,7 @@ Common patterns: | `Core/X` | CC `Packages/CustomControls/Y` | `[Y](../Packages/CustomControls/Y)` | | `Core/X` | CEF `Packages/CEF/Y` | `[Y](../Packages/CEF/Y)` | | `Core/X` | WinEventLogLib `Packages/WinEventLogLib/Y` | `[Y](../Packages/WinEventLogLib/Y)` | +| `Core/X` | WinNamedPipesLib `Packages/WinNamedPipesLib/Y` | `[Y](../Packages/WinNamedPipesLib/Y)` | | `Core/X` | `Core/Y` (sibling) | `[Y](Y)` | Always link to the **canonical** location (the page's `permalink:`), not to a `redirect_from` alias. Pages that have moved out of `Core/` retain a `redirect_from: /tB/Core/` so legacy links still work, but forward-style links should point at the new home. @@ -627,6 +789,7 @@ Always link to the **canonical** location (the page's `permalink:`), not to a `r - CustomControls — framework half: `..\tb-export\NewProject\Packages\CustomControls\Sources\CustomControls.twin` (a single file with `Module Constants`, the interfaces, and the CoClasses). Runtime half: `..\tb-export\NewProject\Packages\CustomControlsPackage\Sources\Waynes.twin` for each control + `zTemporarySupport.twin` for the shared style helpers and the mixin base classes. - CEF package → `..\tbrepro\cef\CEFSampleProject\Packages\cefPackage\Sources\CefControl.twin` for the whole public surface (the `CefBrowser` control, its `CefBrowserBaseCtl` base, and `CefEnvironmentOptions`). For the two surfaced enums: `CEF\Enums\_cef_log_severity_t.twin` (declares both the internal `cef_log_severity_t` and the user-facing `CefLogSeverity`) and `CEF\CrossProcessIPC\BrowserOM.twin` (declares `cefPrintOrientation` inline, around line 29). Everything else under `cefPackage\Sources\` and `cefPackage\Sources\CEF\` is `Private Class` / `Private Module` plumbing — skip. The sample project's `Sources\Example1..4.twin` are the source-of-truth for which features are *not* yet exposed (commented-out event handlers with *"Sorry, this feature is not yet available in the CEF package"*). - WinEventLogLib package → `..\tb-export\NewProject\Packages\WinEventLogLib\Sources\EventLog.twin` (the generic `EventLog(Of T1, T2)` class) and `Helper.twin` (`EventLogHelperPublic.RegisterEventLogInternal`). Skip `APIs.twin` (`Private Module`), `Constants.twin` (`Private Module` — the `EventLogTypeConstants` enum is unreachable from outside the package), and the `EventLogHelperPrivate` module in `Helper.twin` (named "Private" though declared `Public`; only used internally by `EventLog.LogArray`). + - WinNamedPipesLib package → `..\tb-export\NewProject\Packages\WinNamedPipesLib\Sources\` — one `.twin` per public class: `NamedPipeServer.twin`, `NamedPipeServerConnection.twin`, `NamedPipeClientManager.twin`, `NamedPipeClientConnection.twin`. Each `.twin` also declares a `Private Interface INamedPipe*Internal` (refcount / dispatch helper used by the IOCP threads); skip those. Skip `APIs.twin`, `Constants.twin`, and `Helper.twin` (all `Private Module`). 2. **Decide placement**: - Pure language keyword (parsed by the compiler, no runtime call) → `docs/Reference/Core/`. - Runtime function/property → `docs/Reference///`. Add `redirect_from: /tB/Core/` so legacy `tB/Core/` links still work. @@ -641,6 +804,7 @@ Always link to the **canonical** location (the page's `permalink:`), not to a `r - CustomControls enumeration → `docs/Reference/CustomControls/Enumerations/.md` (mirrors `WebView2/Enumerations/` and `VBRUN/Constants/`). The three `Long`-alias enums (`ColorRGBA`, `PixelCount`, `PointSize`) live here too, even though they're really typedefs. - CEF control → `docs/Reference/CEF/CefBrowser/index.md` (folder-style; carries the `EnvironmentOptions` sub-page). Pre-creation options class → `docs/Reference/CEF/CefBrowser/EnvironmentOptions.md` (parallel to `WebView2/WebView2/EnvironmentOptions.md`). CEF enumeration → `docs/Reference/CEF/Enumerations/.md`. - WinEventLogLib generic class → `docs/Reference/WinEventLogLib/EventLog.md` (single-file; the surface is small — constructor + three methods). WinEventLogLib helper module → `docs/Reference/WinEventLogLib/EventLogHelperPublic.md` (single-file; one Sub). + - WinNamedPipesLib class → `docs/Reference/WinNamedPipesLib/.md` (single-file; one page per public class — `NamedPipeServer.md`, `NamedPipeServerConnection.md`, `NamedPipeClientManager.md`, `NamedPipeClientConnection.md`). No folder-style — none of the four classes have sub-pages. - Pick `` from VBA's grouping (Information, Interaction, Strings, FileSystem, DateTime, Math, Financial, Conversion, ...) and the existing folders under `Reference//`. 3. **Adapt content** (VBA-Docs sources): - Strip MS frontmatter (`ms.assetid`, `f1_keywords`, `keywords`, `ms.date`, `ms.localizationpriority`). @@ -699,10 +863,25 @@ Always link to the **canonical** location (the page's `permalink:`), not to a `r - The index page's *Message resources* section should describe what Windows *expects* (a message-table resource in the EXE pointed at by `EventMessageFile`, keyed by the `T1` / `T2` enum values), **not** make strong claims about how twinBASIC delivers it. The `.twin` source does not contain visible `mc.exe`-equivalent emit; whatever populates the resource lives in the compiler's special-handling path for the `[ClassId("…EAEAEAEAEAEA")]` magic-byte pattern, and that is not directly observable from the package's own sources. - The README copy-paste mistake (header says "NAMED PIPES PACKAGE", body is correct) is *not* surfaced on the docs — write the actual description ("a simple framework for creating Windows event log entries"), don't propagate the wrong name. - Omit the `vba_attribution: true` frontmatter flag — these pages are fully original (the package is MIT-licensed, same situation as the other Wayne Phillips packages). -10. **Flag tB deviations** with a `> [!NOTE]` callout (see next section). -11. **Update the parent index** (`//index.md`, `docs/Reference/VB/index.md`, `docs/Reference/WebView2/index.md`, `docs/Reference/Assert/index.md`, `docs/Reference/CustomControls/index.md` (and its `Styles/`, `Framework/`, `Enumerations/` sub-indices), `docs/Reference/CEF/index.md` (and its `Enumerations/` sub-index), `docs/Reference/WinEventLogLib/index.md`, `Reference/Statements.md`, or `Reference/Procedures and Functions.md`) — turn an unlinked bullet into a link with a short blurb. Match the existing style of the page. If a new package is being added, also extend `docs/Reference/Packages.md` to list it. -12. **Add the page** to `Reference/Statements.md` or `Reference/Procedures and Functions.md` if it's a statement or callable and not already listed there. -13. **Run the [site integrity check](#site-integrity-check)** after the batch and before committing. +10. **Adapt content** (WinNamedPipesLib `.twin` sources): + - The four `Public Class …` files are flat — there is no inheritance to walk, so list each class's surface in the order *Fields → Events → Methods*, with members within each group alphabetised. Mirror the shape of `WebView2/WebView2Request.md` (similar size + flat layout). + - `[COMCreatable(False)]` is on every class — mention this on each page's intro paragraph (the consumer reaches `NamedPipeServerConnection` / `NamedPipeClientConnection` through events / return values rather than by `New`). Do not list `[InterfaceId]`, `[ClassId]`, `[EventInterfaceId]` on the page; they are COM-plumbing decoration. + - Each `Public` field carries a `[Description("…")]` attribute — use it as the basis for the field entry, then expand. There is no `[Description]` on events, on methods, or on classes; that prose is fully original. + - The `INamedPipe*Internal` interfaces at the top of each `.twin` are `Private Interface` and only exist so the IOCP worker threads can refcount the corresponding class via `stdole.IUnknown`. **No doc page** — do not surface the underscored implementing properties (`_Handle`, `_IsConnected`, …) either; they're the interface-implementation half of the `Public` field of the same name. + - For events with a `Data() As Byte` parameter (server `ClientMessageReceived`, client `MessageReceived`): document the parameter as **Byte()** but include a `> [!IMPORTANT]` callout saying the array is a transient `SAFEARRAY` view over the IOCP read buffer and must be **copied** if its contents are needed past the event handler. The source uses a hand-rolled `SAFEARRAY_1D` UDT and clears the array pointer at the end of `NotifyClientDataReceived` / `NotifyReceivedDataAsync` — that lifetime is real and trips up consumers who store the array reference. + - The optional *Cookie* parameter on `AsyncRead` / `AsyncWrite` (and the corresponding event parameter) is the package's correlation handle. Document it as a **Variant** opaque token whose value flows through unchanged from the issuing call to the matching `MessageSent` / `MessageReceived` event. Use this as the example pattern when illustrating tracked writes. + - `MessageBufferSize` is the *initial* buffer size, not a cap on message size — the IOCP loop handles `ERROR_MORE_DATA` by allocating a larger buffer and re-issuing the read. Surface this on each `MessageBufferSize` entry (server and client manager). The `_README.txt` TODO *"remove max size 131072 of messages"* suggests a future hard cap removal; surface as a TODO without making a stronger claim. + - `NamedPipeServer.PipeName` must be set before `Start()` (or `Start()` raises run-time error 5). Surface as a `> [!IMPORTANT]` callout on the field entry. + - `NamedPipeServer.Start()` is idempotent (no-op if already started); `Stop()` is idempotent (no-op if not started). Both call back into themselves from `Class_Terminate`. Mention this on each method. + - `NamedPipeClientConnection.AsyncClose()`: the `_README.txt` says *"you MUST call AsyncClose on the client side, otherwise the connection is left alive when the object goes out of scope"*. This is a required-call: surface as a `> [!IMPORTANT]` callout on the class intro AND on the `AsyncClose` method entry. Note that `Class_Terminate` also calls `AsyncClose`, so the contract is technically *"either let the object terminate, or call `AsyncClose` first"* — but the README's wording is what users will look for, so quote it. + - `NamedPipeServer.ManualMessageLoopEnter` / `ManualMessageLoopLeave`: explain why these exist — when `FreeThreadingEvents = False` (the default) events are marshalled to the UI thread via a hidden `STATIC`-class window's `WndProc`, which requires a Win32 message loop to be running. UI hosts (Forms) already pump; services / console hosts don't, so they call `ManualMessageLoopEnter` from their entry point and `ManualMessageLoopLeave` from a shutdown handler. + - `Handle` is `Public` on both connection classes — it is the underlying Win32 pipe handle. Document it as informational (useful for low-level / debugging access), not as something user code typically reads or writes. + - The README TODO list lives on the **landing page** ("Known limitations" or "Roadmap" section), not on the per-class pages. Reproduce only the user-facing items (no method to drop a single client from the server side; no `Error` events on the IOCP worker thread; suspected hard cap on message size). Skip the *"currently a lot of duplicate code in server + client"* note — it's an internal-refactor concern. + - Omit the `vba_attribution: true` frontmatter flag — these pages are fully original (the package is MIT-licensed). +11. **Flag tB deviations** with a `> [!NOTE]` callout (see next section). +12. **Update the parent index** (`//index.md`, `docs/Reference/VB/index.md`, `docs/Reference/WebView2/index.md`, `docs/Reference/Assert/index.md`, `docs/Reference/CustomControls/index.md` (and its `Styles/`, `Framework/`, `Enumerations/` sub-indices), `docs/Reference/CEF/index.md` (and its `Enumerations/` sub-index), `docs/Reference/WinEventLogLib/index.md`, `docs/Reference/WinNamedPipesLib/index.md`, `Reference/Statements.md`, or `Reference/Procedures and Functions.md`) — turn an unlinked bullet into a link with a short blurb. Match the existing style of the page. If a new package is being added, also extend `docs/Reference/Packages.md` to list it. +13. **Add the page** to `Reference/Statements.md` or `Reference/Procedures and Functions.md` if it's a statement or callable and not already listed there. +14. **Run the [site integrity check](#site-integrity-check)** after the batch and before committing. ## twinBASIC deviations from VBA to flag From 74fd63f1070c330d8f33dcc43e815f330997e4da Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Wed, 13 May 2026 22:54:13 +0200 Subject: [PATCH 3/4] Document the WinNamedPipesLib package. --- docs/Reference/Packages.md | 1 + .../NamedPipeClientConnection.md | 147 +++++++++++++ .../NamedPipeClientManager.md | 120 +++++++++++ .../WinNamedPipesLib/NamedPipeServer.md | 193 ++++++++++++++++++ .../NamedPipeServerConnection.md | 103 ++++++++++ docs/Reference/WinNamedPipesLib/index.md | 106 ++++++++++ 6 files changed, 670 insertions(+) create mode 100644 docs/Reference/WinNamedPipesLib/NamedPipeClientConnection.md create mode 100644 docs/Reference/WinNamedPipesLib/NamedPipeClientManager.md create mode 100644 docs/Reference/WinNamedPipesLib/NamedPipeServer.md create mode 100644 docs/Reference/WinNamedPipesLib/NamedPipeServerConnection.md create mode 100644 docs/Reference/WinNamedPipesLib/index.md diff --git a/docs/Reference/Packages.md b/docs/Reference/Packages.md index 088b506..5e94127 100644 --- a/docs/Reference/Packages.md +++ b/docs/Reference/Packages.md @@ -27,3 +27,4 @@ These packages are built into twinBASIC and are always available, even offline. - [CEF Package](CEF/) -- the **CefBrowser** control wrapping the Chromium Embedded Framework: cross-platform-ready browser embedding with a choice of three Chromium runtimes (v49 / v109 / v145); currently in BETA - [WebView2 Package](WebView2/) -- the **WebView2** control wrapping the Microsoft Edge runtime, plus its surrounding wrapper objects (request / response / headers / environment options) and the `wv2…` enumerations - [WinEventLogLib Package](WinEventLogLib/) -- writes Windows Event Log entries from twinBASIC; the generic **EventLog**(*Of EventIds, Categories*) class handles registration, registry setup, and the per-event `ReportEventW` call, with message-table resources for *EventIds* and *Categories* synthesised into the EXE at compile time +- [WinNamedPipesLib Package](WinNamedPipesLib/) -- Windows named pipes as twinBASIC objects with an asynchronous IOCP-driven I/O model; **NamedPipeServer** + **NamedPipeServerConnection** on the host side, **NamedPipeClientManager** + **NamedPipeClientConnection** on the client side, with message-boundary semantics and a cookie-based correlation pattern across `AsyncRead` / `AsyncWrite` and their matching events diff --git a/docs/Reference/WinNamedPipesLib/NamedPipeClientConnection.md b/docs/Reference/WinNamedPipesLib/NamedPipeClientConnection.md new file mode 100644 index 0000000..a7ac650 --- /dev/null +++ b/docs/Reference/WinNamedPipesLib/NamedPipeClientConnection.md @@ -0,0 +1,147 @@ +--- +title: NamedPipeClientConnection +parent: WinNamedPipesLib Package +permalink: /tB/Packages/WinNamedPipesLib/NamedPipeClientConnection +has_toc: false +--- + +# NamedPipeClientConnection class +{: .no_toc } + +One client-side connection to a named pipe. Produced by [**NamedPipeClientManager.Connect**](NamedPipeClientManager#connect). Carries the connection-lifecycle events ([**Connected**](#connected), [**Disconnected**](#disconnected)) and the message events ([**MessageReceived**](#messagereceived), [**MessageSent**](#messagesent)), plus the [**AsyncRead**](#asyncread) / [**AsyncWrite**](#asyncwrite) / [**AsyncClose**](#asyncclose) methods that drive them. + +The class is tagged `[COMCreatable(False)]` and its constructor takes a package-private interface — reach instances only through [**NamedPipeClientManager.Connect**](NamedPipeClientManager#connect). + +> [!IMPORTANT] +> The package `_README.txt` states: *"you MUST call **AsyncClose** on the client side, otherwise the connection is left alive when the object goes out of scope"*. Either call [**AsyncClose**](#asyncclose) explicitly before dropping the last reference, **or** let the object terminate cleanly through its `Class_Terminate` (which calls [**AsyncClose**](#asyncclose) automatically). Holding the reference forever — in a module-level **Collection**, for example — without calling [**AsyncClose**](#asyncclose) keeps the pipe handle open and the IOCP thread alive. + +```tb +Private manager As NamedPipeClientManager +Private WithEvents connection As NamedPipeClientConnection + +Private Sub Form_Load() + Set manager = New NamedPipeClientManager + Set connection = manager.Connect("MyService") +End Sub + +Private Sub connection_Connected() + connection.AsyncWrite StrConv("hello", vbFromUnicode) +End Sub + +Private Sub connection_MessageReceived(ByRef Cookie As Variant, ByRef Data() As Byte) + Debug.Print "reply: " & StrConv(Data, vbUnicode) +End Sub + +Private Sub Form_Unload(Cancel As Integer) + connection.AsyncClose +End Sub +``` + +See the package [overview](.) for the IOCP / event-marshalling architecture, the cookie correlation pattern, and the transient lifetime of `Data() As Byte` inside events. + +* TOC +{:toc} + +## Properties + +### CustomData +{: .no_toc } + +A per-connection opaque slot the consumer can attach state to — typically a session object or a pending-replies dictionary tied to this one connection. **Variant**, default **Empty**. The package never reads or writes this field. + +### Handle +{: .no_toc } + +The underlying Win32 file handle returned by `CreateFileW("\\.\pipe\")`. **LongPtr**. Exposed for low-level / debugging use — most consumers can ignore it. Do not call `CloseHandle` on this value yourself; use [**AsyncClose**](#asyncclose) so the IOCP loop and the parent manager's bookkeeping stay consistent. + +### PipeName +{: .no_toc } + +The leaf pipe name this connection was opened against — the same value that was passed to [**NamedPipeClientManager.Connect**](NamedPipeClientManager#connect). **String**. Read-only in practice; the package sets it from the constructor argument and never changes it. + +## Events + +### Connected +{: .no_toc } + +Fires once the asynchronous `CreateFileW` started by [**NamedPipeClientManager.Connect**](NamedPipeClientManager#connect) has succeeded and the pipe is ready for message exchange. + +Syntax: *connection*_**Connected**() + +### Disconnected +{: .no_toc } + +Fires once the pipe has dropped *and* every outstanding asynchronous I/O against the connection has returned. The connection object is no longer usable for I/O after this event. + +Syntax: *connection*_**Disconnected**() + +### MessageReceived +{: .no_toc } + +Fires when a complete message has been read from the pipe. + +Syntax: *connection*_**MessageReceived**(**ByRef** *Cookie* **As Variant**, **ByRef** *Data*() **As Byte**) + +*Cookie* +: The opaque correlation value originally passed to the [**AsyncRead**](#asyncread) that produced this read — or **Empty** if the read came from the auto-issued reads driven by [**NamedPipeClientManager.ContinuouslyReadFromPipe**](NamedPipeClientManager#continuouslyreadfrompipe). + +*Data* +: The message payload. See [Working with `Data() As Byte` in events](.#working-with-data-as-byte-in-events) on the package overview for the transient-buffer lifetime caveat — copy the bytes out before the handler returns if you need them. + +### MessageSent +{: .no_toc } + +Fires when a previously-issued [**AsyncWrite**](#asyncwrite) has completed. + +Syntax: *connection*_**MessageSent**(**ByRef** *Cookie* **As Variant**) + +*Cookie* +: The opaque correlation value that was passed to the originating [**AsyncWrite**](#asyncwrite) call. + +## Methods + +### AsyncClose +{: .no_toc } + +Cancels every outstanding I/O against this connection and closes the underlying pipe handle. Eventually triggers the [**Disconnected**](#disconnected) event once the cancellation completes. Automatically invoked from `Class_Terminate` when the last reference to the connection drops. + +Syntax: *connection*.**AsyncClose** + +> [!IMPORTANT] +> See the class intro: the README requires that either this method runs (explicitly, or through `Class_Terminate`) before the connection is considered finished. + +### AsyncRead +{: .no_toc } + +Manually issues an asynchronous read against this connection. + +Syntax: *connection*.**AsyncRead** [ *Cookie* [, *OverlappedStruct* ] ] + +*Cookie* +: *optional* A **Variant** correlation value, surfaced as the *Cookie* parameter of the matching [**MessageReceived**](#messagereceived) event. Default **Empty**. + +*OverlappedStruct* +: *optional* A **LongPtr** to a pre-allocated `OVERLAPPED_CUSTOM` structure. **Internal use only** — the IOCP machinery passes this when re-issuing a read after `ERROR_MORE_DATA`. Consumer code should always omit this parameter. + +Only needed when the parent manager's [**ContinuouslyReadFromPipe**](NamedPipeClientManager#continuouslyreadfrompipe) is **False**; otherwise the IOCP loop keeps a read pending automatically and explicit calls are redundant. + +### AsyncWrite +{: .no_toc } + +Sends a message to the server. + +Syntax: *connection*.**AsyncWrite** *Data*() [, *Cookie* ] + +*Data* +: *required* A **Byte()** array carrying the bytes to send. An uninitialised or zero-length array is a no-op. + +*Cookie* +: *optional* A **Variant** correlation value, surfaced as the *Cookie* parameter of the matching [**MessageSent**](#messagesent) event. Default **Empty**. + +Returns immediately; the actual transmission runs through the IOCP loop. The completion fires [**MessageSent**](#messagesent) on this connection. + +## See Also + +- [WinNamedPipesLib package](.) -- overview, IOCP / event-marshalling architecture, cookie pattern, `Data()` lifetime caveat, the **AsyncClose** rule +- [NamedPipeClientManager class](NamedPipeClientManager) -- the manager that produced this connection +- [NamedPipeServerConnection class](NamedPipeServerConnection) -- the server-side counterpart diff --git a/docs/Reference/WinNamedPipesLib/NamedPipeClientManager.md b/docs/Reference/WinNamedPipesLib/NamedPipeClientManager.md new file mode 100644 index 0000000..c90183d --- /dev/null +++ b/docs/Reference/WinNamedPipesLib/NamedPipeClientManager.md @@ -0,0 +1,120 @@ +--- +title: NamedPipeClientManager +parent: WinNamedPipesLib Package +permalink: /tB/Packages/WinNamedPipesLib/NamedPipeClientManager +has_toc: false +--- + +# NamedPipeClientManager class +{: .no_toc } + +The client-side coordinator. Owns a Windows I/O Completion Port and a pool of worker threads shared by every [**NamedPipeClientConnection**](NamedPipeClientConnection) it produces, and hands out connection objects through [**Connect**](#connect). One [**NamedPipeClientManager**](.) typically lives for the lifetime of the consuming process and brokers many connections — to one or several servers — through that shared IOCP infrastructure. Instantiate with **New**. + +Configure the public fields (all four have sensible defaults), call [**Connect**](#connect) for each pipe the application wants to dial, and respond to the [**NamedPipeClientConnection**](NamedPipeClientConnection) events. The first [**Connect**](#connect) lazily creates the completion port and starts the worker threads; subsequent calls reuse them. + +```tb +Private manager As NamedPipeClientManager +Private WithEvents connection As NamedPipeClientConnection + +Private Sub Form_Load() + Set manager = New NamedPipeClientManager + Set connection = manager.Connect("MyService") +End Sub + +Private Sub connection_Connected() + Dim payload() As Byte = StrConv("hello", vbFromUnicode) + connection.AsyncWrite payload +End Sub + +Private Sub Form_Unload(Cancel As Integer) + connection.AsyncClose ' required — see README + manager.Stop ' or just let the manager go out of scope +End Sub +``` + +See the package [overview](.) for the IOCP / event-marshalling architecture, the cookie correlation pattern, the transient lifetime of `Data() As Byte` inside events, and the mandatory `AsyncClose` rule for client connections. + +* TOC +{:toc} + +## Properties + +The four configuration fields are read once on the first [**Connect**](#connect) call and propagated to every [**NamedPipeClientConnection**](NamedPipeClientConnection) created through this manager. Subsequent changes affect connections opened thereafter but **not** connections that already exist — set the fields before the first [**Connect**](#connect). + +### ContinuouslyReadFromPipe +{: .no_toc } + +When **True** (the default), each [**NamedPipeClientConnection**](NamedPipeClientConnection) keeps a read pending against its pipe at all times — every [**MessageReceived**](NamedPipeClientConnection#messagereceived) is followed by an automatic `AsyncRead` issued from inside the IOCP thread. Set to **False** to handle reads one-at-a-time; each [**MessageReceived**](NamedPipeClientConnection#messagereceived) handler must then call [**NamedPipeClientConnection.AsyncRead**](NamedPipeClientConnection#asyncread) to receive the next message. **Boolean**, default **True**. + +### FreeThreadingEvents +{: .no_toc } + +Controls where the [**NamedPipeClientConnection**](NamedPipeClientConnection) events are raised. When **False** (the default), the IOCP worker threads marshal each event to the main UI thread through the manager's hidden message-only window, and the consuming process must be pumping a Win32 message loop. When **True**, events fire directly on whichever IOCP worker thread received the completion — no message-loop dependency, but the consumer's event handlers must be thread-safe. **Boolean**, default **False**. + +### MessageBufferSize +{: .no_toc } + +The size, in bytes, of the per-completion `ReadFile` buffer initially allocated for each client connection. **Long**, default **131072** (128 KiB). Does not cap the maximum message size — on `ERROR_MORE_DATA` the IOCP loop allocates a larger overflow buffer and re-issues the read — but the initial size affects throughput for sustained large-message traffic. + +### NumThreadsIOCP +{: .no_toc } + +The number of IOCP worker threads created when [**Connect**](#connect) is first called. **Long**, default **1**. One thread is enough for most scenarios; raise this to allow concurrent event handlers under [**FreeThreadingEvents**](#freethreadingevents) = **True**, or to keep up with heavy traffic on multi-core hardware. + +## Methods + +### Connect +{: .no_toc } + +Opens an asynchronous connection to a named pipe on the local machine. + +Syntax: *manager*.**Connect**( *PipeName* ) **As NamedPipeClientConnection** + +*PipeName* +: *required* The leaf name of the pipe to dial — the package prepends `\\.\pipe\` itself. Raises run-time error 5 *"cannot start without specifying a pipe name"* if empty. + +Lazy on first call: creates the completion port and starts [**NumThreadsIOCP**](#numthreadsiocp) worker threads. Returns immediately with a [**NamedPipeClientConnection**](NamedPipeClientConnection) in the not-yet-connected state. The actual `CreateFileW` runs asynchronously on an IOCP worker and fires [**Connected**](NamedPipeClientConnection#connected) on the returned object once the pipe is open. + +Raises run-time error 5 *"unable to create an IOCP port"* if `CreateIoCompletionPort` fails on the first call. + +### FindNamedPipes +{: .no_toc } + +Enumerates the named pipes currently published on the local machine. + +Syntax: *manager*.**FindNamedPipes** ( [ *Pattern* ] ) **As Collection** + +*Pattern* +: *optional* A wildcard pattern matched against the leaf pipe name (no `\\.\pipe\` prefix; the package adds it). `*` matches any sequence, `?` matches any single character. Default `"*"` — return every pipe. + +Returns a **Collection** of **String** values, each a leaf pipe name suitable to pass to [**Connect**](#connect). Useful as a discovery step when the consumer doesn't know the exact server name in advance: + +```tb +Dim names As Collection = manager.FindNamedPipes("MyService_*") +Dim name As Variant +For Each name In names + Debug.Print "found: " & name +Next +``` + +### Stop +{: .no_toc } + +Cancels every outstanding I/O on every connection produced by this manager, posts the IOCP shutdown sentinel to each worker, waits for the threads to exit, closes every pipe handle, and frees the completion port. Idempotent: calling [**Stop**](#stop) on a manager that has not connected anything — or has already been stopped — is a no-op. Automatically invoked from `Class_Terminate`, so a manager going out of scope cleans up implicitly. + +Syntax: *manager*.**Stop** + +[**NamedPipeClientConnection**](NamedPipeClientConnection) objects produced by this manager remain valid as references after [**Stop**](#stop), but their underlying pipe handles are closed and they cannot perform I/O. + +### New +{: .no_toc } + +Constructs a manager in the not-yet-connected state. Creates the hidden `STATIC`-class message window used to marshal IOCP-thread completions back to the UI thread. + +Syntax: **New NamedPipeClientManager** + +## See Also + +- [WinNamedPipesLib package](.) -- overview, IOCP / event-marshalling architecture, cookie pattern, `Data()` lifetime caveat, **AsyncClose** rule +- [NamedPipeClientConnection class](NamedPipeClientConnection) -- the per-connection object returned by [**Connect**](#connect) +- [NamedPipeServer class](NamedPipeServer) -- the server-side counterpart diff --git a/docs/Reference/WinNamedPipesLib/NamedPipeServer.md b/docs/Reference/WinNamedPipesLib/NamedPipeServer.md new file mode 100644 index 0000000..ccbad72 --- /dev/null +++ b/docs/Reference/WinNamedPipesLib/NamedPipeServer.md @@ -0,0 +1,193 @@ +--- +title: NamedPipeServer +parent: WinNamedPipesLib Package +permalink: /tB/Packages/WinNamedPipesLib/NamedPipeServer +has_toc: false +--- + +# NamedPipeServer class +{: .no_toc } + +Hosts one named pipe and accepts an unbounded number of concurrent client connections, each represented by a [**NamedPipeServerConnection**](NamedPipeServerConnection). The class owns a Windows I/O Completion Port and a configurable pool of worker threads that drive every connection's reads, writes, and connect notifications. Instantiate with **New**. + +Configure the public fields ([**PipeName**](#pipename) is required, the others have sensible defaults), call [**Start**](#start), and respond to the lifecycle events as clients arrive and exchange messages. The package opens the underlying pipe as **PIPE_TYPE_MESSAGE** / **PIPE_READMODE_MESSAGE** — message boundaries on the wire match message boundaries seen by the consumer. + +```tb +Private WithEvents server As NamedPipeServer + +Private Sub Form_Load() + Set server = New NamedPipeServer + server.PipeName = "MyService" + server.Start +End Sub + +Private Sub server_ClientConnected(Connection As NamedPipeServerConnection) + Debug.Print "client " & Connection.Handle & " arrived" +End Sub + +Private Sub server_ClientMessageReceived( _ + Connection As NamedPipeServerConnection, _ + ByRef Cookie As Variant, _ + ByRef Data() As Byte) + Connection.AsyncWrite Data ' echo it back +End Sub +``` + +See the package [overview](.) for the IOCP / event-marshalling architecture, the cookie correlation pattern, and the transient lifetime of `Data() As Byte` inside events. + +* TOC +{:toc} + +## Properties + +### ContinuouslyReadFromPipe +{: .no_toc } + +When **True** (the default), the server keeps a read pending against every connected client at all times — every [**ClientMessageReceived**](#clientmessagereceived) is followed by an automatic `AsyncRead` issued from inside the IOCP thread. Set to **False** to handle reads one-at-a-time; each [**ClientMessageReceived**](#clientmessagereceived) handler must then call [**NamedPipeServerConnection.AsyncRead**](NamedPipeServerConnection#asyncread) to receive the next message. **Boolean**, default **True**. + +### FreeThreadingEvents +{: .no_toc } + +Controls where the lifecycle and message events are raised. When **False** (the default), the IOCP worker threads marshal each event to the main UI thread through a hidden message-only window, and the consuming process must be pumping a Win32 message loop. When **True**, events fire directly on whichever IOCP worker thread received the completion — no message-loop dependency, but the consumer's event handlers must be thread-safe. **Boolean**, default **False**. + +Set this before calling [**Start**](#start); it is read once when the worker threads are created and propagated to every [**NamedPipeServerConnection**](NamedPipeServerConnection). + +### MessageBufferSize +{: .no_toc } + +The size, in bytes, of the per-completion `ReadFile` buffer initially allocated for each connection. **Long**, default **131072** (128 KiB). Does not cap the maximum message size — on `ERROR_MORE_DATA` the IOCP loop allocates a larger overflow buffer and re-issues the read — but the initial size affects how often that overflow path runs, and so affects throughput for sustained large-message traffic. + +### NumThreadsIOCP +{: .no_toc } + +The number of IOCP worker threads created by [**Start**](#start). **Long**, default **1**. One thread is enough for most scenarios because every blocking call inside the worker is an overlapped Win32 operation that releases the thread immediately. Raise this to allow multiple [**ClientMessageReceived**](#clientmessagereceived) handlers to run concurrently under [**FreeThreadingEvents**](#freethreadingevents) = **True**, or to keep up with heavy traffic on multi-core hardware. Set this before calling [**Start**](#start). + +### PipeName +{: .no_toc } + +The name the pipe is published under. **String**, no default. The Win32 pipe namespace path is `\\.\pipe\` — the package prepends `\\.\pipe\` itself; pass just the leaf name. + +> [!IMPORTANT] +> [**PipeName**](#pipename) must be set to a non-empty value before [**Start**](#start), or [**Start**](#start) raises run-time error 5 (*"cannot start without specifying a pipe name"*). + +## Events + +### ClientConnected +{: .no_toc } + +Fires after a client's `ConnectNamedPipe` has completed and the connection is ready for message exchange. + +Syntax: *server*_**ClientConnected**(*Connection* **As NamedPipeServerConnection**) + +*Connection* +: The newly-connected client's server-side connection object. Hold the reference if you need per-client state across messages — the same instance is passed to every event for this client. Note that **Cookie** / `Tag`-style storage is available through [**NamedPipeServerConnection.CustomData**](NamedPipeServerConnection#customdata). + +### ClientDisconnected +{: .no_toc } + +Fires once the client has dropped *and* every outstanding asynchronous I/O against the connection has returned. The connection object is no longer usable for I/O after this event. + +Syntax: *server*_**ClientDisconnected**(*Connection* **As NamedPipeServerConnection**) + +*Connection* +: The connection that has just shut down. Its [**IsConnected**](NamedPipeServerConnection#isconnected) is **False**. + +### ClientMessageReceived +{: .no_toc } + +Fires when a complete message has been read from the pipe. + +Syntax: *server*_**ClientMessageReceived**(*Connection* **As NamedPipeServerConnection**, **ByRef** *Cookie* **As Variant**, **ByRef** *Data*() **As Byte**) + +*Connection* +: The connection the message came from. + +*Cookie* +: The opaque correlation value originally passed to the [**NamedPipeServerConnection.AsyncRead**](NamedPipeServerConnection#asyncread) that produced this read — or **Empty** if the read came from the auto-issued reads driven by [**ContinuouslyReadFromPipe**](#continuouslyreadfrompipe). + +*Data* +: The message payload. See [Working with `Data() As Byte` in events](.#working-with-data-as-byte-in-events) on the package overview for the transient-buffer lifetime caveat — copy the bytes out before the handler returns if you need them. + +### ClientMessageSent +{: .no_toc } + +Fires when a previously-issued [**NamedPipeServerConnection.AsyncWrite**](NamedPipeServerConnection#asyncwrite) has completed (or when an [**AsyncBroadcast**](#asyncbroadcast) message reaches each individual client). + +Syntax: *server*_**ClientMessageSent**(*Connection* **As NamedPipeServerConnection**, **ByRef** *Cookie* **As Variant**) + +*Connection* +: The connection the write went out on. + +*Cookie* +: The opaque correlation value that was passed to the originating [**AsyncWrite**](NamedPipeServerConnection#asyncwrite) call. + +### ServerReady +{: .no_toc } + +Fires once, after [**Start**](#start), when every IOCP worker thread has joined the completion-port loop and the first connection listener is published. Use this as the "the server is now accepting connections" signal. + +Syntax: *server*_**ServerReady**() + +## Methods + +### AsyncBroadcast +{: .no_toc } + +Issues an [**AsyncWrite**](NamedPipeServerConnection#asyncwrite) against every currently-connected client. + +Syntax: *server*.**AsyncBroadcast** *Data*() [, *Cookie* ] + +*Data* +: *required* The message bytes to send. + +*Cookie* +: *optional* A **Variant** correlation value, attached to *each* per-client [**ClientMessageSent**](#clientmessagesent) event. Default **Empty**. + +The set of recipients is snapshotted under a lock at the start of the call. Clients connecting after the snapshot do not receive this broadcast; clients disconnecting after the snapshot but before their per-client write completes simply fail that individual write silently. + +### ManualMessageLoopEnter +{: .no_toc } + +Runs a Win32 message loop on the calling thread until [**ManualMessageLoopLeave**](#manualmessageloopleave) is called from another thread (or any handler raises a `WM_USER_QUITTING` posting). + +Syntax: *server*.**ManualMessageLoopEnter** + +Intended for console / service hosts that do not have a Forms-style message pump of their own but want the default ([**FreeThreadingEvents**](#freethreadingevents) = **False**) marshalled-event semantics. UI hosts already pump messages naturally and do not need this method. + +### ManualMessageLoopLeave +{: .no_toc } + +Posts a `WM_USER_QUITTING` message to the hidden marshalling window, causing the [**ManualMessageLoopEnter**](#manualmessageloopenter) loop on the other thread to break out cleanly. Safe to call from any thread. + +Syntax: *server*.**ManualMessageLoopLeave** + +### Start +{: .no_toc } + +Creates the I/O Completion Port, spins up [**NumThreadsIOCP**](#numthreadsiocp) worker threads, and publishes the first connection listener under `\\.\pipe\`. Fires [**ServerReady**](#serverready) when every worker has joined. + +Syntax: *server*.**Start** + +Raises run-time error 5 *"cannot start without specifying a pipe name"* if [**PipeName**](#pipename) is empty, or *"unable to create an IOCP port"* if `CreateIoCompletionPort` fails. + +Idempotent: calling [**Start**](#start) while the server is already running is a no-op. + +### Stop +{: .no_toc } + +Cancels every outstanding I/O on every connection, posts the IOCP shutdown sentinel to each worker, waits for the threads to exit, closes every pipe handle, and frees the completion port. Idempotent: calling [**Stop**](#stop) on a server that has not been started — or has already been stopped — is a no-op. Automatically invoked from `Class_Terminate`, so a server going out of scope cleans up implicitly. + +Syntax: *server*.**Stop** + +### New +{: .no_toc } + +Constructs a server in the not-yet-started state. Creates the hidden `STATIC`-class message window used to marshal IOCP-thread completions back to the UI thread. + +Syntax: **New NamedPipeServer** + +## See Also + +- [WinNamedPipesLib package](.) -- overview, IOCP / event-marshalling architecture, cookie pattern, `Data()` lifetime caveat, known limitations +- [NamedPipeServerConnection class](NamedPipeServerConnection) -- the per-client connection passed to every event +- [NamedPipeClientManager class](NamedPipeClientManager) -- the client-side counterpart diff --git a/docs/Reference/WinNamedPipesLib/NamedPipeServerConnection.md b/docs/Reference/WinNamedPipesLib/NamedPipeServerConnection.md new file mode 100644 index 0000000..32702be --- /dev/null +++ b/docs/Reference/WinNamedPipesLib/NamedPipeServerConnection.md @@ -0,0 +1,103 @@ +--- +title: NamedPipeServerConnection +parent: WinNamedPipesLib Package +permalink: /tB/Packages/WinNamedPipesLib/NamedPipeServerConnection +has_toc: false +--- + +# NamedPipeServerConnection class +{: .no_toc } + +One server-side per-client connection. A [**NamedPipeServer**](NamedPipeServer) creates one of these for every client that connects, and surfaces it as the *Connection* parameter of every server event. Use it to send messages to that specific client, to manually issue reads when [**NamedPipeServer.ContinuouslyReadFromPipe**](NamedPipeServer#continuouslyreadfrompipe) is **False**, and to close the connection from the server side. + +The class is tagged `[COMCreatable(False)]` and its constructor takes a package-private interface — reach instances only through [**NamedPipeServer**](NamedPipeServer) events. Connection-lifecycle and message events come through the parent [**NamedPipeServer**](NamedPipeServer); this class carries the per-connection data and methods only. + +```tb +Private Sub server_ClientConnected(Connection As NamedPipeServerConnection) + ' attach per-client state through the CustomData slot + Connection.CustomData = New ClientSession +End Sub + +Private Sub server_ClientMessageReceived( _ + Connection As NamedPipeServerConnection, _ + ByRef Cookie As Variant, _ + ByRef Data() As Byte) + + Dim session As ClientSession = Connection.CustomData + session.HandleMessage Data +End Sub +``` + +See the package [overview](.) for the IOCP / event-marshalling architecture, the cookie correlation pattern, and the transient lifetime of `Data() As Byte` inside events. + +* TOC +{:toc} + +## Properties + +### CustomData +{: .no_toc } + +A per-connection opaque slot the consumer can attach state to — typically a session object scoped to that one client. **Variant**, default **Empty**. The package never reads or writes this field; it is provided for convenience so that consumers do not have to maintain a parallel `Dictionary` keyed by [**Handle**](#handle). + +### Handle +{: .no_toc } + +The underlying Win32 named-pipe handle. **LongPtr**. Exposed for low-level / debugging use — most consumers can ignore it. Do not call `CloseHandle` on this value yourself; use [**AsyncClose**](#asyncclose) so the IOCP loop and the parent server's bookkeeping stay consistent. + +### IsConnected +{: .no_toc } + +**True** between the client connecting and the connection dropping. **Boolean**. Set internally; consumer code typically reads this rather than writes it. Becomes **False** as soon as the underlying pipe drops, even before the [**ClientDisconnected**](NamedPipeServer#clientdisconnected) event fires (the event waits until every outstanding I/O has returned). + +### IsOpening +{: .no_toc } + +**True** during the brief window between the package creating the connection object and `ConnectNamedPipe` completing. **Boolean**. Used internally by [**NamedPipeServer.Stop**](NamedPipeServer#stop) to avoid a race condition during shutdown; consumer code does not normally need to read it. + +## Methods + +### AsyncClose +{: .no_toc } + +Cancels every outstanding I/O against this connection and closes the underlying pipe handle. Eventually triggers a [**ClientDisconnected**](NamedPipeServer#clientdisconnected) event on the parent server once the cancellation completes. Automatically invoked from `Class_Terminate` when the last reference to the connection drops. + +Syntax: *connection*.**AsyncClose** + +### AsyncRead +{: .no_toc } + +Manually issues an asynchronous read against this connection. + +Syntax: *connection*.**AsyncRead** [ *Cookie* [, *OverlappedStruct* ] ] + +*Cookie* +: *optional* A **Variant** correlation value, surfaced as the *Cookie* parameter of the matching [**ClientMessageReceived**](NamedPipeServer#clientmessagereceived) event. Default **Empty**. + +*OverlappedStruct* +: *optional* A **LongPtr** to a pre-allocated `OVERLAPPED_CUSTOM` structure. **Internal use only** — the IOCP machinery passes this when re-issuing a read after `ERROR_MORE_DATA`. Consumer code should always omit this parameter. + +Only needed when the parent server's [**ContinuouslyReadFromPipe**](NamedPipeServer#continuouslyreadfrompipe) is **False**; otherwise the IOCP loop keeps a read pending automatically and explicit calls are redundant. + +### AsyncWrite +{: .no_toc } + +Sends a message back to this specific client. + +Syntax: *connection*.**AsyncWrite** *Data*() [, *Cookie* ] + +*Data* +: *required* A **Byte()** array carrying the bytes to send. An uninitialised or zero-length array is a no-op. + +*Cookie* +: *optional* A **Variant** correlation value, surfaced as the *Cookie* parameter of the matching [**ClientMessageSent**](NamedPipeServer#clientmessagesent) event. Default **Empty**. + +Returns immediately; the actual transmission runs through the IOCP loop. The completion fires [**ClientMessageSent**](NamedPipeServer#clientmessagesent) on the parent server. + +To send the same message to every connected client at once, use [**NamedPipeServer.AsyncBroadcast**](NamedPipeServer#asyncbroadcast). + +## See Also + +- [WinNamedPipesLib package](.) -- overview, IOCP / event-marshalling architecture, cookie pattern, `Data()` lifetime caveat +- [NamedPipeServer class](NamedPipeServer) -- the parent server that owns this connection +- [NamedPipeClientConnection class](NamedPipeClientConnection) -- the client-side counterpart diff --git a/docs/Reference/WinNamedPipesLib/index.md b/docs/Reference/WinNamedPipesLib/index.md new file mode 100644 index 0000000..01df3a8 --- /dev/null +++ b/docs/Reference/WinNamedPipesLib/index.md @@ -0,0 +1,106 @@ +--- +title: WinNamedPipesLib Package +parent: Packages +grand_parent: Reference Section +nav_order: 9 +permalink: /tB/Packages/WinNamedPipesLib/ +has_toc: false +--- + +# WinNamedPipesLib Package +{: .no_toc } + +The **WinNamedPipesLib** built-in package exposes Windows named pipes as twinBASIC objects with an asynchronous, IOCP-driven I/O model. One process hosts a [**NamedPipeServer**](NamedPipeServer); other processes use a [**NamedPipeClientManager**](NamedPipeClientManager) to open one or more [**NamedPipeClientConnection**](NamedPipeClientConnection) instances to it. Writes complete in the background; messages and connection-lifecycle changes are delivered as events. + +The package is a built-in package shipped with twinBASIC. Add it through Project → References (**Ctrl-T**) → Available Packages. + +* TOC +{:toc} + +## Architecture + +Two halves, each one user-instantiated coordinator class plus one per-connection class: + +| Side | Coordinator | Per-connection | +|---------|--------------------------------------------------------|----------------------------------------------------------------| +| Server | [**NamedPipeServer**](NamedPipeServer) | [**NamedPipeServerConnection**](NamedPipeServerConnection) | +| Client | [**NamedPipeClientManager**](NamedPipeClientManager) | [**NamedPipeClientConnection**](NamedPipeClientConnection) | + +The server publishes a name (`PipeName = "MyService"` → Win32 path `\\.\pipe\MyService`) and hands out a [**NamedPipeServerConnection**](NamedPipeServerConnection) for every client that connects. The client manager dials by the same name (with [**Connect**](NamedPipeClientManager#connect)) and gets back a [**NamedPipeClientConnection**](NamedPipeClientConnection). The two ends are symmetric thereafter — both expose `AsyncRead`, `AsyncWrite`, and `AsyncClose` with the same signatures. + +Reads, writes, and connection completion all run through the same Windows I/O Completion Port (IOCP) infrastructure. Each coordinator class owns its own completion port, a configurable pool of worker threads ([**NumThreadsIOCP**](NamedPipeServer#numthreadsiocp)), and a hidden message-only window used to marshal events back to the UI thread. + +## Event delivery — marshalled vs free-threaded + +By default events fire on the main UI thread. The IOCP worker threads receive each completion, package the buffer, and `PostMessage` the result to a hidden `STATIC`-class window owned by the coordinator. The window's subclassed `WndProc` then raises the BASIC event from the message loop. This means the consuming process **must be pumping a Win32 message loop** for events to be delivered. Forms-based hosts already are; console hosts and Windows services are not, and need either [**NamedPipeServer.ManualMessageLoopEnter**](NamedPipeServer#manualmessageloopenter) (and the matching `ManualMessageLoopLeave`) or `FreeThreadingEvents = True`. + +Setting [**FreeThreadingEvents**](NamedPipeServer#freethreadingevents) to **True** skips the marshalling round-trip and raises events directly from the IOCP worker thread. Performance is higher and there is no message-loop requirement, but the consumer's event handlers must be thread-safe — multiple `ClientMessageReceived` events from different clients can fire concurrently, and global / class state touched from the handler is **not** protected by the implicit UI-thread serialisation that the default mode gives. + +The flag must be set before [**Start**](NamedPipeServer#start) (server side) or before the first [**Connect**](NamedPipeClientManager#connect) call (client side); it is read once and propagated to every per-connection object. + +## Asynchronous reads + +When [**ContinuouslyReadFromPipe**](NamedPipeServer#continuouslyreadfrompipe) is **True** (the default), the package keeps a read pending against every connection at all times — every [**ClientMessageReceived**](NamedPipeServer#clientmessagereceived) / [**MessageReceived**](NamedPipeClientConnection#messagereceived) event is followed by another `AsyncRead` issued from inside the IOCP thread. Set the flag to **False** to handle reads one-at-a-time: each event handler must then call [**NamedPipeServerConnection.AsyncRead**](NamedPipeServerConnection#asyncread) / [**NamedPipeClientConnection.AsyncRead**](NamedPipeClientConnection#asyncread) to receive the next message. This is useful for back-pressure when the consumer can't process messages as fast as they arrive. + +## The cookie correlation pattern + +Every [**AsyncRead**](NamedPipeServerConnection#asyncread) and [**AsyncWrite**](NamedPipeServerConnection#asyncwrite) accepts an optional *Cookie* of type **Variant**. Whatever value the caller passes in is round-tripped through the IOCP completion and re-emitted as the *Cookie* parameter of the matching [**ClientMessageReceived**](NamedPipeServer#clientmessagereceived) / [**ClientMessageSent**](NamedPipeServer#clientmessagesent) (or client-side [**MessageReceived**](NamedPipeClientConnection#messagereceived) / [**MessageSent**](NamedPipeClientConnection#messagesent)) event. Use this to correlate event callbacks with the calls that initiated them — a per-request sequence number, a callback object, a key into a pending-replies dictionary. + +```tb +Private pending As New Collection + +Private Sub SendRequest(text As String, replyHandler As IReplyHandler) + Dim cookie As Long = NextCookie() + pending.Add replyHandler, CStr(cookie) + connection.AsyncWrite Encode(text), cookie +End Sub + +Private Sub connection_MessageReceived(ByRef Cookie As Variant, ByRef Data() As Byte) + Dim handler As IReplyHandler = pending(CStr(Cookie)) + pending.Remove CStr(Cookie) + handler.HandleReply Decode(Data) +End Sub +``` + +## Working with `Data() As Byte` in events + +The *Data* parameter on [**ClientMessageReceived**](NamedPipeServer#clientmessagereceived) and [**MessageReceived**](NamedPipeClientConnection#messagereceived) is **not** a normal heap-allocated **Byte** array. The package constructs a hand-rolled `SAFEARRAY` whose backing memory points at the IOCP read buffer, then clears the array pointer at the end of the event handler so the buffer can be recycled. The values are valid *only* while the handler is on the stack. + +> [!IMPORTANT] +> Copy the bytes out before the event handler returns if you need them later. Storing the array reference in a module-level variable, a **Collection**, or a class field leaves a dangling pointer once the IOCP loop reuses the buffer for the next message. + +For a fresh **Byte()** copy: + +```tb +Dim Stored() As Byte +ReDim Stored(UBound(Data)) +[_HiddenModule].vbaCopyBytes UBound(Data) + 1, VarPtr(Stored(0)), VarPtr(Data(0)) +``` + +For a text payload, `StrConv(Data, vbUnicode)` (UTF-8) or `CStr` over a `vbUnicode`-converted copy reads the bytes immediately and produces an owned **String** in one step. + +## Closing a client connection + +> [!IMPORTANT] +> The **`_README.txt`** states: *"you MUST call **AsyncClose** on the client side, otherwise the connection is left alive when the object goes out of scope"*. + +Either let the [**NamedPipeClientConnection**](NamedPipeClientConnection) object terminate cleanly through its `Class_Terminate` (which calls [**AsyncClose**](NamedPipeClientConnection#asyncclose) automatically) **or** call [**AsyncClose**](NamedPipeClientConnection#asyncclose) yourself before dropping the last reference. Holding a reference forever — for example in a long-lived module-level **Collection** — without calling [**AsyncClose**](NamedPipeClientConnection#asyncclose) keeps the underlying pipe handle open and the IOCP thread alive. + +## Discovering pipes + +[**NamedPipeClientManager.FindNamedPipes**](NamedPipeClientManager#findnamedpipes) enumerates the named pipes published on the local machine, returning a **Collection** of **String** names that match an optional `*`/`?` wildcard pattern. The implementation is `FindFirstFileW("\\.\pipe\")` — the package strips the leading namespace itself, so pass just the pipe name (`"MyService*"`, not `"\\.\pipe\MyService*"`). + +## Known limitations + +These are open TODOs documented in the package's `_README.txt`. They are user-visible — surface them when designing a protocol on top of the package: + +- **No server-side disconnect of a single client.** [**NamedPipeServer.Stop**](NamedPipeServer#stop) tears down the whole pipe. There is no per-[**NamedPipeServerConnection**](NamedPipeServerConnection) `Close` method on the public surface yet, so a server cannot drop one misbehaving client while keeping the others connected. +- **No `Error` event on the IOCP worker thread.** Errors inside the IOCP loop currently surface as VBA run-time errors on whichever worker thread is running, not as a marshalled `Error` event on any of the four classes. Wrap I/O calls in **On Error Resume Next** where you need to keep the worker thread running on failure. +- **Message-size cap.** The author's TODO list flags *"remove max size 131072 of messages"*. [**MessageBufferSize**](NamedPipeServer#messagebuffersize) is the initial buffer size and the IOCP loop *does* grow it on `ERROR_MORE_DATA`, so messages larger than 128 KiB will work — but the TODO suggests there is a hard cap somewhere the author wants to remove. Treat very large messages as a roadmap item, not a guarantee. + +## Classes + +- [NamedPipeServer](NamedPipeServer) -- the server: publishes a pipe name, hosts an IOCP loop, raises events for the lifecycle of every accepted client +- [NamedPipeServerConnection](NamedPipeServerConnection) -- one server-side per-client connection; the **Connection** parameter of every `NamedPipeServer` event, with its own `AsyncRead` / `AsyncWrite` / `AsyncClose` +- [NamedPipeClientManager](NamedPipeClientManager) -- the client-side coordinator; owns the IOCP loop and the [**Connect**](NamedPipeClientManager#connect) / [**Stop**](NamedPipeClientManager#stop) / [**FindNamedPipes**](NamedPipeClientManager#findnamedpipes) methods +- [NamedPipeClientConnection](NamedPipeClientConnection) -- one client-side connection; carries the `Connected` / `Disconnected` / `MessageReceived` / `MessageSent` events and the matching `AsyncRead` / `AsyncWrite` / `AsyncClose` methods From 0831471155ccb409470952afc1bf585686cf81f8 Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Wed, 13 May 2026 22:58:59 +0200 Subject: [PATCH 4/4] Update the WIP. --- WIP.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/WIP.md b/WIP.md index 9c90d12..70e86d6 100644 --- a/WIP.md +++ b/WIP.md @@ -865,7 +865,10 @@ Always link to the **canonical** location (the page's `permalink:`), not to a `r - Omit the `vba_attribution: true` frontmatter flag — these pages are fully original (the package is MIT-licensed, same situation as the other Wayne Phillips packages). 10. **Adapt content** (WinNamedPipesLib `.twin` sources): - The four `Public Class …` files are flat — there is no inheritance to walk, so list each class's surface in the order *Fields → Events → Methods*, with members within each group alphabetised. Mirror the shape of `WebView2/WebView2Request.md` (similar size + flat layout). - - `[COMCreatable(False)]` is on every class — mention this on each page's intro paragraph (the consumer reaches `NamedPipeServerConnection` / `NamedPipeClientConnection` through events / return values rather than by `New`). Do not list `[InterfaceId]`, `[ClassId]`, `[EventInterfaceId]` on the page; they are COM-plumbing decoration. + - `[COMCreatable(False)]` is on every class, but its user-facing implication differs by role: + - On the *coordinator* classes (`NamedPipeServer`, `NamedPipeClientManager`) the attribute only blocks late-binding `CreateObject`; user code still instantiates with `New`. **Do not mention** `[COMCreatable(False)]` on these pages — write `"Instantiate with **New**."` instead. (Note: `Reference/Attributes.md` describes `[COMCreatable(True)]` as *"this coclass can be created with the **New** keyword"*, which suggests `False` blocks `New`. That description is misleading — `New` from a project that references the package works on `[COMCreatable(False)]` classes; the attribute only governs external COM creation. Don't propagate the misleading wording.) + - On the *Connection* classes (`NamedPipeServerConnection`, `NamedPipeClientConnection`) the attribute pairs with the design choice that user code doesn't construct these directly — each `Sub New` takes a package-`Private Interface` parameter, so the constructor is effectively unreachable from outside the package anyway. Mention `[COMCreatable(False)]` on the intro paragraph the same way [`CustomControls/WaynesTextBox/WaynesTextBoxState.md`](docs/Reference/CustomControls/WaynesTextBox/WaynesTextBoxState.md) does: *"The class is tagged `[COMCreatable(False)]` and its constructor takes a package-private interface — reach instances only through …"*. + - Do not list `[InterfaceId]`, `[ClassId]`, `[EventInterfaceId]` on the page; they are COM-plumbing decoration. - Each `Public` field carries a `[Description("…")]` attribute — use it as the basis for the field entry, then expand. There is no `[Description]` on events, on methods, or on classes; that prose is fully original. - The `INamedPipe*Internal` interfaces at the top of each `.twin` are `Private Interface` and only exist so the IOCP worker threads can refcount the corresponding class via `stdole.IUnknown`. **No doc page** — do not surface the underscored implementing properties (`_Handle`, `_IsConnected`, …) either; they're the interface-implementation half of the `Public` field of the same name. - For events with a `Data() As Byte` parameter (server `ClientMessageReceived`, client `MessageReceived`): document the parameter as **Byte()** but include a `> [!IMPORTANT]` callout saying the array is a transient `SAFEARRAY` view over the IOCP read buffer and must be **copied** if its contents are needed past the event handler. The source uses a hand-rolled `SAFEARRAY_1D` UDT and clears the array pointer at the end of `NotifyClientDataReceived` / `NotifyReceivedDataAsync` — that lifetime is real and trips up consumers who store the array reference.