etw-wrapper 0.1.0

Strongly typed Event Tracing for Windows (ETW) logger generation
docs.rs failed to build etw-wrapper-0.1.0
Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.

etw-wrapper

Strongly-typed Event Tracing for Windows (ETW) logger generator for Rust.

Point the gen_etw_wrapper! macro at an ETW manifest file to emit a struct with a method for each event.

use etw_wrapper::{gen_etw_wrapper, FILETIME};

gen_etw_wrapper!("manifests/widgetservice.man", PROVIDER_WIDGETSERVICE -> WidgetLogger);

fn main() -> etw_wrapper::Result<()> {
    let logger = WidgetLogger::register()?;

    // Signatures are derived from the manifest templates
    logger.service_started("1.0.0", 8, FILETIME::default())?;
    logger.request_failed(0x12ABCDEF, 500, 42, "failed to succeed")?;

    Ok(()) // provider is unregistered automatically when `logger` is dropped
}

[!NOTE] This crate is Windows-only. Use cfg gates if you need to use this alongside code for other platforms.

Getting started

Add the crate to your Cargo.toml:

[dependencies]

etw-wrapper = "0.1.0"

The gen_etw_wrapper! macro is available by default through the macro feature. If you only want the runtime pieces, disable default features:

[dependencies]

etw-wrapper = { version = "0.1.0", default-features = false }

See the Manual API section for how to use the helpers directly.

Usage

Generating a provider

Invoke the macro at module scope with the path to your manifest. Paths are resolved relative to the crate's CARGO_MANIFEST_DIR (i.e. the directory containing your Cargo.toml).

This will generate a struct using the default naming convention of [PascalCaseProvideSymbol]Logger. For example the symbol PROVIDER_WIDGETSERVICE becomes ProviderWidgetserviceLogger.

gen_etw_wrapper!("manifests/widgetservice.man");

To pick your own struct name, map the provider's manifest symbol to it with ->:

gen_etw_wrapper!("manifests/widgetservice.man", PROVIDER_WIDGETSERVICE -> WidgetLogger);

If the provider symbol is not a valid Rust identifier, quote it:

gen_etw_wrapper!("manifests/widgetservice.man", "Contoso.WidgetService" -> WidgetLogger);

Emitting events

Given this manifest template and event:

<template tid="T_ServiceStarted">
  <data name="Version"     inType="win:UnicodeString"/>
  <data name="WorkerCount" inType="win:UInt32"/>
  <data name="StartTime"   inType="win:FILETIME"/>
</template>

<event value="1" symbol="SERVICE_STARTED" channel="OperationalChannel"
       level="win:Informational" template="T_ServiceStarted"/>

the macro generates:

impl WidgetLogger {
    pub fn register() -> etw_wrapper::Result<Self> { /* ... */ }

    pub fn service_started(&self, version: &str, worker_count: u32, start_time: FILETIME)
        -> etw_wrapper::Result<()> { /* ... */ }
}

[!IMPORTANT] The macro emits events but does not build or register the provider's message and metadata resource tables. Consumers will need the manifest registered seperately. This can be handled via a combination of mc.exe and wevtutil.exe.

For a simple way of compiling and embedding the resources, use a build-time helper such as embed-resource.

Type mapping

The macro maps manifest inType values to Rust parameter types as follows:

Manifest inType Rust type Notes
win:Int8 i8
win:UInt8 u8
win:Int16 i16
win:UInt16 u16
win:Int32 i32
win:UInt32, win:HexInt32 u32
win:Int64 i64
win:UInt64, win:HexInt64 u64
win:Float f32
win:Double f64
win:Boolean bool Encoded as a 32-bit Windows BOOL
win:Pointer usize
win:GUID GUID
win:FILETIME FILETIME
win:SYSTEMTIME SYSTEMTIME
win:UnicodeString &str Interior NULs become spaces, length="N" produces exactly N units including NUL
win:AnsiString &[u8] Caller supplies provider-code-page bytes and the final NUL
win:AnsiString with outType="win:Utf8/win:Json/win:Xml" &str Encoded as UTF-8, interior NULs become spaces
win:Binary &[u8] length="N" becomes &[u8; N], length="Field" derives that field (see below)
win:SID &field::Sid Build from a PSID via unsafe Sid::from_psid, or borrow an owned SidBuf

Variable-Length Fields

Binary

When a win:Binary field uses length="OtherField", the raw event structure expects to receive the length in that field. The generated method does not expose this field. It is derived internally from the slice's length.

As an example:

<data name="BlobSize" inType="win:UInt32"/>
<data name="Blob"     inType="win:Binary" length="BlobSize"/>

generates the method

fn blob_written(&self, blob: &[u8]) -> Result<()>

Strings

For win:UnicodeString length="N", the generated method emits exactly N UTF-16 code units as required by ETW: at most N-1 content units, space padding when needed, and a terminating NUL.

A default win:AnsiString uses the provider's ANSI code page, so its generated parameter remains raw bytes. A fixed-length default ANSI field becomes &[u8; N]; the array must already contain the required padding and final NUL. If the manifest explicitly selects outType="win:Utf8", win:Json, or win:Xml, the generated method accepts &str, performs UTF-8 encoding, and handles fixed-length padding and truncation.

Manual API

If you prefer not to use the macro, you can use the runtime directly. The EtwLogger automatically handles the internal callbacks and ensures that deregistration happens when the logger is dropped. It can be moved and stored freely.

The field module provides converters for passing fields to the write function safely: scalar for any Copy value; str16/to_u16cstring and to_u16cstring_fixed_len for UTF-16 strings; str8/to_cstring and to_cstring_fixed_len for ANSI strings; bytes for binary blobs; and sid for a Sid.

Before logging you can call enabled() to check that your event write won't be a no-op.

use etw_wrapper::{EVENT_DESCRIPTOR, EtwLogger, GUID, field::*};

let ctx = EtwLogger::register(&GUID::from_u128(0x8B3A1F42_6C7D_4E9A_9F21_3D5E0A7C1B84))?;

let descriptor = EVENT_DESCRIPTOR { Id: 1, Level: 4, ..Default::default() };

if ctx.enabled(descriptor.Level, descriptor.Keyword) {
    let version = to_u16cstring("1.0.0");
    let workers = 8u32;
    ctx.write(&descriptor, &[str16(&version), scalar(&workers)])?;
}

License

Licensed under the ISC License.