See WIP.md for the cross-package maintenance guide. Sister packages: WinServicesLib, WinNamedPipesLib.
The user-facing surface is the generic EventLog(Of T1, T2) class plus a single helper module. The package's Constants.twin declares Public Enum EventLogTypeConstants inside a Private Module, so the enum does not actually surface — and the EventLogHelperPrivate module in Helper.twin is intended-private despite the Public modifier (only used by EventLog.LogArray internally). The Win32 API wrappers in APIs.twin are pure plumbing.
Public user-facing surface (one generic class + one helper module):
| Symbol | Kind | Role |
|---|---|---|
EventLog(Of T1, T2) |
Generic class | Main user-facing class. T1 is the event-ID enum, T2 is the category enum. |
EventLogHelperPublic |
Public module | Holds the low-level RegisterEventLogInternal helper. |
RegisterEventLogInternal |
Sub on the module |
Registry-write helper; EventLog.Register() is the normal entry point. |
EventLog(Of T1, T2) public members:
Public Sub New(LogName As String)— constructor.LogNameis either a leaf name ("MyService", registered underApplication\MyService) or a full path ("System\MyService", registered underSystem\MyService).Public Sub LogSuccess(ByVal EventId As T1, ByVal CategoryId As T2, ParamArray AdditionalStrings())— writes an Information-type event (EVENTLOG_SUCCESS = 0). The name "Success" is the Win32 SDK constant's literal name, not the audit-success category — the underlying event type is Information.Public Sub LogFailure(ByVal EventId As T1, ByVal CategoryId As T2, ParamArray AdditionalStrings())— writes an Error-type event (EVENTLOG_ERROR_TYPE = 1).Public Sub Register()— writes the registry entries underHKLM\SYSTEM\CurrentControlSet\Services\EventLog\…to declare this EXE as the message provider for the source. CallsRegisterEventLogInternal(LogName, GetDeclaredMaxEnumValue(Of T2))— the category count is derived fromT2's declared maximum value at compile time.
Class-level decoration on EventLog: [COMCreatable(False)], [ClassId("4AEA12E8-…-EAEAEAEAEAEA")] (the EA suffix triggers special compiler handling for generic classes). The [Description] attribute on the class is the basis for the page intro: "This is the main event log (generic) class."
EventLogHelperPublic public members:
Public Sub RegisterEventLogInternal(ByVal LogPath As String, ByVal CategoryCount As Long)— the registry-write helper. Prepends"Application\"to LogPath if no backslash is present, opensHKLM\SYSTEM\CurrentControlSet\Services\EventLog\<LogPath>withKEY_WRITE, then writesEventMessageFileandCategoryMessageFile(both set toApp.ModulePath) andCategoryCount. Requires admin rights (registry writes to HKLM). Raises run-time error 5 with the message "Failed to register event log source (<LogName>)" if the open fails. Normally callers useEventLog.Register(), which fills CategoryCount automatically.
Gaps and quirks to surface on the docs (drawn from a static read of the source):
- The
EventLogTypeConstantsenum has five values (Success,Warning,Error,AuditSuccess,AuditFailure) but the public class only exposes Information and Error event types — Warning and Audit events are not currently reachable. - Method names follow the Win32 SDK constants verbatim:
LogSuccesswrites an Information event (becauseEVENTLOG_SUCCESS = 0is the Win32 spelling for the information type), andLogFailurewrites an Error event. Call this out on the per-method entries. - Message resources: the registry entries point at
App.ModulePath(the running EXE) for bothEventMessageFileandCategoryMessageFile. Windows therefore expects a message-table resource keyed by theT1andT2enum values to be embedded in the EXE. The.twinsource does not itself synthesise that resource; whatever mechanism populates the resource sits in the compiler's special-handling path for the[ClassId("…EAEAEAEAEAEA")]magic-byte pattern. The docs describe what Windows expects without making strong claims about how the compiler delivers it. Register()requires elevation. Normal usage is to call it once during install (from an elevated installer), then callLogSuccess/LogFailureat runtime without elevation.
The package's intended usage pattern — not obvious from the bare API — is composition-delegation:
Class TBSERVICE001
Implements ITbService
Implements EventLog(Of MESSAGETABLE.EVENTS, MESSAGETABLE.CATEGORIES) Via _
EventLog = New EventLog(Of MESSAGETABLE.EVENTS, MESSAGETABLE.CATEGORIES)("Application\" & CurrentComponentName)
…
LogSuccess(service_started, status_changed, CurrentComponentName) ' surfaces directly
End Class
The Implements <Class> Via <field> = <expression> form is twinBASIC's composition-delegation syntax (see docs/Features/Language/Delegation.md if/once that page exists, or the CustomControls mixin pattern for an analogous use). The class declares it Implements EventLog(Of …) and gives the compiler a private field plus a constructor expression; the compiler then auto-forwards every Public member of EventLog (LogSuccess, LogFailure, Register) through that field. The result: a service class that contains an EventLog instance and exposes its logging methods as if they were its own.
Surface this on the EventLog page (and on the package index) as the recommended pattern for service / long-running classes. Spell out:
- The constructor expression evaluates once (the first time the delegating class is instantiated, per twinBASIC's
Implements ... Viasemantics). - The
T1/T2type arguments must be identical at theImplementsdeclaration and the constructor (the compiler enforces this). - The
LogPathis typically"Application\" & CurrentComponentName—CurrentComponentNameis the compile-time class name, so the log path automatically tracks renames. - The delegating class transparently inherits all three of
LogSuccess/LogFailure/Register. Calling code can use them unqualified.
The T1 / T2 enums are typically auto-populated from a JSON resource via the [PopulateFrom] attribute:
Module MESSAGETABLE
[PopulateFrom("json", "/Resources/MESSAGETABLE/Strings.json", "events", "name", "id")]
Enum EVENTS
End Enum
[PopulateFrom("json", "/Resources/MESSAGETABLE/Strings.json", "categories", "name", "id")]
Enum CATEGORIES
End Enum
End Module
…with Resources\MESSAGETABLE\Strings.json:
{
"events": [
{ "id": -1073610751, "name": "service_started", "LCID_0000": "%1 service started" },
{ "id": -1073610750, "name": "service_startup_failed", "LCID_0000": "%1 service startup failed" },
…
],
"categories": [
{ "id": 1, "name": "status_changed", "LCID_0000": "Status Changed" }
]
}Two things are happening here:
- The enum bodies are populated at compile time —
Enum EVENTSstarts empty in the source, but after compilation it has membersservice_started = -1073610751,service_startup_failed = -1073610750, … (one per"events"entry in the JSON, keyedname → id). - The same JSON is consumed by the compiler's
mc.exe-equivalent that emits the message-table resource intoApp.ModulePath. TheLCID_0000strings are the message-table entries, and the%1,%2, … placeholders are filled at log time from theAdditionalStringsParamArraytoLogSuccess/LogFailure. TheCategoryCountregistry value (written byRegister()) is the highest declaredidin thecategoriesblock, which is whatGetDeclaredMaxEnumValue(Of T2)recovers at compile time.
So the round-trip is: JSON → compile-time enum population + message-table resource emission → registry entries that point Windows at the EXE → runtime LogSuccess(EventId, CategoryId, …) writes an event the Event Viewer can format using the embedded message-table strings.
Surface this on the index page (under "Setting up message resources" or similar) with the JSON skeleton and the cross-reference to [PopulateFrom] (which is documented under docs/Features/, not in the reference set — link to that page if it exists, otherwise describe in-place).
The negative event-ID values in the JSON (-1073610751) are the standard Win32 event-ID encoding: the high bits encode severity (0xC0000000 = Error), facility (0x...), and customer bit. Don't unpack this on the docs; just note that "event IDs follow the Win32 documented encoding — see Microsoft's 'Event Identifiers' reference".
A class can only Implements EventLog(Of T1, T2) Via … once. If a service needs events from multiple unrelated message tables, it can compose multiple EventLog instances as named fields (no Via), accepting a small loss of ergonomics (calls become MyEventLog.LogSuccess(…) instead of LogSuccess(…)). Surface this as a one-line note on the index — most services share a single MESSAGETABLE module across all their classes, so the limitation rarely bites.