winlog 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,788 @@
1
+ /*
2
+ * winlog — structured, registration-free ETW TraceLogging emit for Ruby.
3
+ *
4
+ * The suite's FIRST C++ extension, by necessity: a runtime-dynamic
5
+ * log(level, event, **fields) API cannot use TraceLoggingProvider.h (which
6
+ * needs compile-time-constant provider/event/field names). Microsoft's
7
+ * supported answer for runtime-dynamic events is TraceLoggingDynamic.h, which
8
+ * is C++-only (templates + std::vector). It is vendored verbatim into
9
+ * ext/winlog/TraceLoggingDynamic.h, MIT-licensed (c) Microsoft Corporation,
10
+ * from microsoft/tracelogging (etw/cpp/traceloggingdynamic). The header is a
11
+ * point-in-time copy (see plans/research/tracelogging.md §1/§12 for provenance:
12
+ * the same file built and ran in the research spike on this machine).
13
+ *
14
+ * ----------------------------------------------------------------------------
15
+ * C++ / rb_raise HYGIENE (the lithos discipline, adapted — see §3.6 of the
16
+ * spec). Invariants, in order of precedence:
17
+ * 1. No C++ exception ever crosses into Ruby frames. Every region that runs
18
+ * tld / std::vector code is wrapped try { ... } catch (...) and converted
19
+ * to rb_raise from a frame that holds no live C++ object.
20
+ * 2. No rb_raise longjmp ever crosses a frame holding a live C++ object. The
21
+ * tld::Event is heap-allocated (new), owned by the _write frame as a raw
22
+ * pointer, and delete'd on EVERY exit path before any raise. No
23
+ * stack-scoped C++ objects with destructors live in any frame a longjmp
24
+ * can cross.
25
+ * 3. All argument coercion / validation happens up front, before the
26
+ * tld::Event exists.
27
+ *
28
+ * GVL STRATEGY — every call HOLDS the GVL (documented deviation, spec §3.5).
29
+ * Nothing in winlog blocks on the kernel:
30
+ * | call | GVL | why |
31
+ * | EventRegister + EventSetInformation | hold | µs-scale; MUST serialize |
32
+ * | (via tld::Provider ctor) | | vs unregister; GVL=lock |
33
+ * | EventWriteTransfer (tld::Event::Write) | hold | non-blocking buffer copy |
34
+ * | EventUnregister (close / GC free) | hold | same as register |
35
+ * | EventActivityIdControl(CREATE_ID) | hold | trivial |
36
+ * Consequence: no ubf, no OVERLAPPED, no cancellation logic anywhere.
37
+ *
38
+ * INCLUDE-ORDER TRAP (mswin, research §2): <ruby.h> MUST precede <windows.h>.
39
+ * ruby/win32.h pulls <winsock2.h>; a bare <windows.h> first pulls <winsock.h>
40
+ * -> C2375 redefinition storm. Never name an identifier IN/OUT. mkmf already
41
+ * injects -D_WIN32_WINNT=_WIN32_WINNT_WIN8 (verified) — do not redefine it.
42
+ *
43
+ * E15 (DLL-unload crash hazard) / E16 (fork): not reachable on MRI. MRI never
44
+ * FreeLibrary's an extension .so before process exit, winlog has no DllMain,
45
+ * and every other path unregisters first (explicit close, GC free hook). fork
46
+ * does not exist on mswin. Nothing to implement (spec §5 E15/E16).
47
+ */
48
+
49
+ #include <ruby.h>
50
+ #include <ruby/encoding.h>
51
+
52
+ #define WIN32_LEAN_AND_MEAN
53
+ #include <windows.h>
54
+ #include <evntprov.h>
55
+
56
+ #include <vector>
57
+ #include "TraceLoggingDynamic.h"
58
+
59
+ #include <limits.h>
60
+ #include <string.h>
61
+
62
+ typedef tld::Event<std::vector<BYTE> > DynEvent;
63
+
64
+ /* ------------------------------------------------------------------ globals */
65
+
66
+ static VALUE mWinlog;
67
+ static VALUE cProvider;
68
+ static VALUE eError; /* Winlog::Error < StandardError */
69
+ static VALUE eClosed; /* Winlog::Closed < Winlog::Error */
70
+
71
+ /* Reserved keyword bits 48..63 (Microsoft-defined). Mirrors the Ruby constant
72
+ * Winlog::KEYWORD_RESERVED_MASK (spec §2.1 / E3). */
73
+ #define WINLOG_KEYWORD_RESERVED_MASK 0xFFFF000000000000ULL
74
+
75
+ /* ---------------------------------------------------------- native struct */
76
+
77
+ typedef struct {
78
+ tld::Provider *prov; /* heap; owns REGHANDLE + provider metadata blob */
79
+ int closed;
80
+ } provider_t;
81
+
82
+ static void
83
+ provider_free(void *p)
84
+ {
85
+ provider_t *pt = (provider_t *)p;
86
+ /* The tld::Provider destructor calls EventUnregister and HeapFree's the
87
+ * metadata blob. Finalizer is a safety net, never the API (spec §3.4):
88
+ * explicit idempotent close + Winlog.open's block form exist. */
89
+ if (pt->prov) {
90
+ delete pt->prov;
91
+ pt->prov = NULL;
92
+ }
93
+ xfree(pt);
94
+ }
95
+
96
+ static size_t
97
+ provider_memsize(const void *p)
98
+ {
99
+ const provider_t *pt = (const provider_t *)p;
100
+ /* Safe even after a failed registration: InitFail frees the real blob and
101
+ * points m_pbMetadata at the static 3-byte NullMetadata, so
102
+ * GetMetadataSize() returns 3 (not a dangling read). */
103
+ return sizeof(provider_t) +
104
+ (pt->prov ? (size_t)pt->prov->GetMetadataSize() : 0);
105
+ }
106
+
107
+ static const rb_data_type_t provider_type = {
108
+ "Winlog::Provider",
109
+ { 0, provider_free, provider_memsize, }, /* no dmark: no VALUEs in struct */
110
+ 0, 0, RUBY_TYPED_FREE_IMMEDIATELY,
111
+ };
112
+
113
+ static VALUE
114
+ provider_alloc(VALUE klass)
115
+ {
116
+ provider_t *pt;
117
+ VALUE obj = TypedData_Make_Struct(klass, provider_t, &provider_type, pt);
118
+ pt->prov = NULL; /* a GC'd never-initialized object is safe to free */
119
+ pt->closed = 0;
120
+ return obj;
121
+ }
122
+
123
+ /* Raw getter (close/closed?/inspect must work on closed objects). */
124
+ static provider_t *
125
+ provider_get(VALUE self)
126
+ {
127
+ provider_t *pt;
128
+ TypedData_Get_Struct(self, provider_t, &provider_type, pt);
129
+ return pt;
130
+ }
131
+
132
+ /* Live getter: raises Winlog::Closed on a closed or never-registered object
133
+ * (E19/E20). An .allocate'd instance has prov == NULL -> Closed, never a crash. */
134
+ static provider_t *
135
+ provider_live(VALUE self)
136
+ {
137
+ provider_t *pt = provider_get(self);
138
+ if (pt->prov == NULL || pt->closed)
139
+ rb_raise(eClosed, "winlog: provider is closed");
140
+ return pt;
141
+ }
142
+
143
+ /* ------------------------------------------------------------- GUID format */
144
+
145
+ /* GUID -> 36-char lowercase hyphenated UTF-8 String. */
146
+ static VALUE
147
+ guid_to_rb(GUID const &g)
148
+ {
149
+ char buf[40];
150
+ snprintf(buf, sizeof buf,
151
+ "%08lx-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x",
152
+ (unsigned long)g.Data1, g.Data2, g.Data3,
153
+ g.Data4[0], g.Data4[1], g.Data4[2], g.Data4[3],
154
+ g.Data4[4], g.Data4[5], g.Data4[6], g.Data4[7]);
155
+ return rb_utf8_str_new_cstr(buf);
156
+ }
157
+
158
+ /* Parse a strict 36-char lowercase-or-uppercase hyphenated GUID String into a
159
+ * GUID. Returns 1 on success, 0 on any deviation from the canonical form
160
+ * (E29: braced {...}, hyphenless, wrong length all rejected). Case-insensitive
161
+ * per the spec ("activity: 36-char hyphenated GUID String (case-insensitive)").
162
+ * Pure C parsing — no Ruby allocation, safe to call before any C++ object. */
163
+ static int
164
+ parse_guid(VALUE str, GUID *out)
165
+ {
166
+ const char *s;
167
+ long len;
168
+ int i;
169
+ unsigned int b[16]; /* the 16 data bytes, in display order */
170
+ static const int hyphen_at[4] = { 8, 13, 18, 23 };
171
+ int bi = 0;
172
+
173
+ Check_Type(str, T_STRING);
174
+ s = RSTRING_PTR(str);
175
+ len = RSTRING_LEN(str);
176
+ if (len != 36)
177
+ return 0;
178
+
179
+ for (i = 0; i < 36; i++) {
180
+ char c = s[i];
181
+ if (i == hyphen_at[0] || i == hyphen_at[1] ||
182
+ i == hyphen_at[2] || i == hyphen_at[3]) {
183
+ if (c != '-') return 0;
184
+ continue;
185
+ }
186
+ if (!((c >= '0' && c <= '9') ||
187
+ (c >= 'a' && c <= 'f') ||
188
+ (c >= 'A' && c <= 'F')))
189
+ return 0;
190
+ }
191
+
192
+ /* Re-scan, packing hex pairs into 16 bytes (display byte order). */
193
+ {
194
+ int pos = 0;
195
+ while (pos < 36 && bi < 16) {
196
+ if (s[pos] == '-') { pos++; continue; }
197
+ {
198
+ char pair[3];
199
+ pair[0] = s[pos];
200
+ pair[1] = s[pos + 1];
201
+ pair[2] = 0;
202
+ b[bi++] = (unsigned int)strtoul(pair, NULL, 16);
203
+ pos += 2;
204
+ }
205
+ }
206
+ if (bi != 16)
207
+ return 0;
208
+ }
209
+
210
+ /* Display order is big-endian for Data1/2/3; assemble accordingly. */
211
+ out->Data1 = ((DWORD)b[0] << 24) | ((DWORD)b[1] << 16) |
212
+ ((DWORD)b[2] << 8) | (DWORD)b[3];
213
+ out->Data2 = (WORD)(((unsigned)b[4] << 8) | (unsigned)b[5]);
214
+ out->Data3 = (WORD)(((unsigned)b[6] << 8) | (unsigned)b[7]);
215
+ for (i = 0; i < 8; i++)
216
+ out->Data4[i] = (BYTE)b[8 + i];
217
+ return 1;
218
+ }
219
+
220
+ /* ------------------------------------------------- field name / event name */
221
+
222
+ /* Transcode a String to UTF-8 with String#encode semantics (the strict path):
223
+ * - already UTF-8: returned as-is (the caller checks coderange/NUL).
224
+ * - other encoding: rb_str_encode, which RAISES Ruby's own
225
+ * Encoding::UndefinedConversionError on a byte with no UTF-8 mapping (E22) —
226
+ * unlike rb_str_export_to_enc, which silently substitutes. This matches the
227
+ * spec's "bytes with no UTF-8 mapping raise Encoding::UndefinedConversionError".
228
+ * Raises from a clean frame (no C++ object live). */
229
+ static VALUE
230
+ transcode_utf8(VALUE str)
231
+ {
232
+ rb_encoding *u8 = rb_utf8_encoding();
233
+ if (rb_enc_get(str) == u8)
234
+ return str;
235
+ return rb_str_encode(str, rb_enc_from_encoding(u8), 0, Qnil);
236
+ }
237
+
238
+ /* Coerce a Symbol/String name to a UTF-8 String, validating: non-empty, no
239
+ * embedded NUL (ETW requirement, E6), no broken UTF-8 coderange (E22). Raises
240
+ * ArgumentError/TypeError from a clean frame (no C++ object live). +what+ is
241
+ * "event name" or a field name for the message. Returns the (possibly new)
242
+ * UTF-8 String VALUE; the caller must keep it referenced while using its ptr. */
243
+ static VALUE
244
+ coerce_name_utf8(VALUE v, const char *what)
245
+ {
246
+ VALUE str;
247
+
248
+ if (SYMBOL_P(v)) {
249
+ str = rb_sym2str(v); /* already UTF-8 */
250
+ } else if (RB_TYPE_P(v, T_STRING)) {
251
+ str = v;
252
+ } else {
253
+ rb_raise(rb_eTypeError, "winlog: %s must be a String or Symbol, got %" PRIsVALUE,
254
+ what, rb_obj_class(v));
255
+ }
256
+
257
+ str = transcode_utf8(str);
258
+
259
+ if (RSTRING_LEN(str) == 0)
260
+ rb_raise(rb_eArgError, "winlog: %s must not be empty", what);
261
+ if (rb_enc_str_coderange(str) == ENC_CODERANGE_BROKEN)
262
+ rb_raise(rb_eArgError, "winlog: %s is not valid UTF-8", what);
263
+ if (memchr(RSTRING_PTR(str), '\0', RSTRING_LEN(str)) != NULL)
264
+ rb_raise(rb_eArgError, "winlog: %s must not contain a NUL byte", what);
265
+
266
+ return str;
267
+ }
268
+
269
+ /* =================================================================
270
+ * Field building (the hot path). add_field_i runs under rb_hash_foreach,
271
+ * itself under rb_protect, with the tld::Event owned on the heap OUTSIDE this
272
+ * frame. add_field_i MAY rb_raise directly (TypeError/ArgumentError/RangeError
273
+ * with the field name): the longjmp unwinds through rb_hash_foreach
274
+ * (longjmp-safe) and is caught by rb_protect, crossing NO frame that holds a
275
+ * live C++ object. Every tld:: call here is individually try/catch(...)-wrapped
276
+ * so std::bad_alloc never unwinds through rb_hash_foreach's bookkeeping.
277
+ * =================================================================*/
278
+
279
+ struct build_ctx {
280
+ DynEvent *ev;
281
+ int oom; /* set if any tld:: call threw (std::bad_alloc etc.) */
282
+ };
283
+
284
+ /* GC-compaction rule (spec §3.6): do every Ruby-side op (Symbol->String,
285
+ * transcode) FIRST, then take RSTRING_PTR and immediately copy into the event.
286
+ * The tld Add* calls are pure C++ (no Ruby allocation between pointer fetch and
287
+ * copy), GVL held throughout, so no concurrent GC moves the buffer.
288
+ *
289
+ * COMPACTION PITFALL (do not reintroduce): RSTRING_PTR(fname_str) must be taken
290
+ * only AFTER each branch's LAST Ruby allocation, never once at the top. In the
291
+ * text branch transcode_utf8(val) calls rb_str_encode, a Ruby allocation that
292
+ * can trigger GC compaction and relocate fname_str's character buffer; a fname
293
+ * pointer captured before it would be stale when AddString does strlen+memcpy.
294
+ * So fname is fetched immediately before the AddField/AddString pair in every
295
+ * branch, with no Ruby allocation in between. RB_GC_GUARD pins fname_str against
296
+ * collection; the late fetch handles movement. */
297
+ static int
298
+ add_field_i(VALUE key, VALUE val, VALUE arg)
299
+ {
300
+ struct build_ctx *ctx = (struct build_ctx *)arg;
301
+ DynEvent *ev = ctx->ev;
302
+ VALUE fname_str = coerce_name_utf8(key, "field name");
303
+ const char *fname; /* fetched late, per-branch — see header note */
304
+
305
+ switch (TYPE(val)) {
306
+ case T_STRING: {
307
+ if (rb_enc_get_index(val) == rb_ascii8bit_encindex()) {
308
+ /* BINARY -> TypeBinary (UINT16-counted). E7: > 65535 bytes. */
309
+ long n = RSTRING_LEN(val);
310
+ if (n > 0xFFFF) {
311
+ fname = RSTRING_PTR(fname_str);
312
+ rb_raise(rb_eArgError,
313
+ "winlog: binary field %s is %ld bytes (max 65535)",
314
+ fname, n);
315
+ }
316
+ /* No Ruby allocation past this point in this branch. */
317
+ fname = RSTRING_PTR(fname_str);
318
+ try {
319
+ ev->AddField(fname, tld::TypeBinary);
320
+ ev->AddBinary(RSTRING_PTR(val), (UINT16)n);
321
+ } catch (...) { ctx->oom = 1; return ST_STOP; }
322
+ } else {
323
+ /* text -> TypeUtf8String (NUL-terminated). Transcode + validate
324
+ * (E6 embedded NUL would silently truncate; E22 broken UTF-8;
325
+ * unmappable bytes raise Encoding::UndefinedConversionError).
326
+ * transcode_utf8 may allocate (rb_str_encode) and move fname_str,
327
+ * so fname is fetched only AFTER it. */
328
+ VALUE utf8 = transcode_utf8(val);
329
+ if (rb_enc_str_coderange(utf8) == ENC_CODERANGE_BROKEN) {
330
+ fname = RSTRING_PTR(fname_str);
331
+ rb_raise(rb_eArgError,
332
+ "winlog: text field %s is not valid UTF-8", fname);
333
+ }
334
+ if (memchr(RSTRING_PTR(utf8), '\0', RSTRING_LEN(utf8)) != NULL) {
335
+ fname = RSTRING_PTR(fname_str);
336
+ rb_raise(rb_eArgError,
337
+ "winlog: text field %s contains a NUL byte "
338
+ "(use a binary string for raw bytes)", fname);
339
+ }
340
+ {
341
+ /* fname and cstr fetched back-to-back, no allocation between
342
+ * here and the Add* copy. */
343
+ fname = RSTRING_PTR(fname_str);
344
+ const char *cstr = RSTRING_PTR(utf8);
345
+ try {
346
+ ev->AddField(fname, tld::TypeUtf8String);
347
+ ev->AddString(cstr);
348
+ } catch (...) { ctx->oom = 1; return ST_STOP; }
349
+ RB_GC_GUARD(utf8);
350
+ }
351
+ }
352
+ break;
353
+ }
354
+ case T_FIXNUM:
355
+ case T_BIGNUM: {
356
+ INT64 v = (INT64)NUM2LL(val); /* RangeError outside INT64 (E9) */
357
+ fname = RSTRING_PTR(fname_str);
358
+ try {
359
+ ev->AddField(fname, tld::TypeInt64);
360
+ ev->AddValue(v);
361
+ } catch (...) { ctx->oom = 1; return ST_STOP; }
362
+ break;
363
+ }
364
+ case T_FLOAT: {
365
+ double v = NUM2DBL(val);
366
+ fname = RSTRING_PTR(fname_str);
367
+ try {
368
+ ev->AddField(fname, tld::TypeDouble);
369
+ ev->AddValue(v);
370
+ } catch (...) { ctx->oom = 1; return ST_STOP; }
371
+ break;
372
+ }
373
+ case T_TRUE:
374
+ case T_FALSE: {
375
+ INT32 v = (val == Qtrue) ? 1 : 0;
376
+ fname = RSTRING_PTR(fname_str);
377
+ try {
378
+ ev->AddField(fname, tld::TypeBool32);
379
+ ev->AddValue(v);
380
+ } catch (...) { ctx->oom = 1; return ST_STOP; }
381
+ break;
382
+ }
383
+ default:
384
+ fname = RSTRING_PTR(fname_str);
385
+ rb_raise(rb_eTypeError,
386
+ "winlog: unsupported value type for field %s "
387
+ "(String, Integer, Float, true, or false; "
388
+ "nil and others are rejected)", fname);
389
+ }
390
+
391
+ RB_GC_GUARD(fname_str);
392
+ return ST_CONTINUE;
393
+ }
394
+
395
+ struct foreach_args {
396
+ VALUE fields;
397
+ struct build_ctx *ctx;
398
+ };
399
+
400
+ static VALUE
401
+ build_fields_protected(VALUE a)
402
+ {
403
+ struct foreach_args *fa = (struct foreach_args *)a;
404
+ rb_hash_foreach(fa->fields, add_field_i, (VALUE)fa->ctx);
405
+ return Qnil;
406
+ }
407
+
408
+ /* Core build + write. UNGATED. Returns the HRESULT as an Integer. Validation of
409
+ * the event name and control args happens BEFORE the heap event exists; field
410
+ * values are validated inside add_field_i (only reached here, i.e. only when
411
+ * the caller decided to build). The tld::Event is heap-owned by THIS frame and
412
+ * delete'd on every exit path before any raise (hygiene invariant 2). */
413
+ static VALUE
414
+ do_write(provider_t *pt, VALUE event, VALUE fields,
415
+ ULONGLONG keyword, UCHAR level, UCHAR opcode,
416
+ GUID *pActivity, GUID *pRelated)
417
+ {
418
+ VALUE ev_name = coerce_name_utf8(event, "event name");
419
+ const char *name = RSTRING_PTR(ev_name);
420
+ DynEvent *ev = NULL;
421
+ HRESULT hr;
422
+ int state = 0;
423
+ struct build_ctx ctx;
424
+ struct foreach_args fa;
425
+
426
+ /* Construct the heap event. The ctor builds metadata via std::vector and
427
+ * can throw std::bad_alloc; nothing is leaked on throw (ctor cleanup). */
428
+ try {
429
+ ev = new DynEvent(name, level, keyword,
430
+ 0 /*tags*/, opcode, 0 /*task*/,
431
+ pActivity, pRelated);
432
+ } catch (...) {
433
+ ev = NULL;
434
+ }
435
+ RB_GC_GUARD(ev_name); /* keep name alive across the ctor */
436
+ if (ev == NULL)
437
+ rb_raise(rb_eNoMemError, "winlog: out of memory building event");
438
+
439
+ ctx.ev = ev;
440
+ ctx.oom = 0;
441
+ fa.fields = fields;
442
+ fa.ctx = &ctx;
443
+
444
+ if (!NIL_P(fields))
445
+ rb_protect(build_fields_protected, (VALUE)&fa, &state);
446
+
447
+ if (state) { delete ev; rb_jump_tag(state); } /* re-raise clean */
448
+ if (ctx.oom) {
449
+ delete ev;
450
+ rb_raise(rb_eNoMemError, "winlog: out of memory building event");
451
+ }
452
+
453
+ try {
454
+ hr = ev->Write(*pt->prov);
455
+ } catch (...) {
456
+ delete ev;
457
+ rb_raise(rb_eNoMemError, "winlog: out of memory writing event");
458
+ }
459
+
460
+ delete ev;
461
+ return INT2NUM((int)hr);
462
+ }
463
+
464
+ /* --------------------------------------------------- control-arg helpers */
465
+
466
+ /* level: Symbol already mapped to Integer by the Ruby layer; here we accept an
467
+ * Integer 1..255 (E5: 0 rejected). Raises ArgumentError from a clean frame. */
468
+ static UCHAR
469
+ level_to_uchar(VALUE vlevel)
470
+ {
471
+ long lv = NUM2LONG(vlevel);
472
+ if (lv < 1 || lv > 255)
473
+ rb_raise(rb_eArgError,
474
+ "winlog: level must be 1..255 (got %ld); 0 is not a valid "
475
+ "ETW level", lv);
476
+ return (UCHAR)lv;
477
+ }
478
+
479
+ /* keyword: Integer 0..0x0000_FFFF_FFFF_FFFF. Reserved bits 48..63 -> ArgError
480
+ * (E3). Negative -> ArgError. Non-Integer is rejected by NUM2ULL/Check before. */
481
+ static ULONGLONG
482
+ keyword_to_ull(VALUE vkw)
483
+ {
484
+ /* Reject negatives explicitly: NUM2ULL would wrap them. */
485
+ if (RB_TYPE_P(vkw, T_FIXNUM) || RB_TYPE_P(vkw, T_BIGNUM)) {
486
+ if (rb_funcall(vkw, rb_intern("<"), 1, INT2FIX(0)) == Qtrue)
487
+ rb_raise(rb_eArgError, "winlog: keyword must not be negative");
488
+ } else {
489
+ rb_raise(rb_eTypeError, "winlog: keyword must be an Integer");
490
+ }
491
+ {
492
+ ULONGLONG kw = (ULONGLONG)NUM2ULL(vkw);
493
+ if (kw & WINLOG_KEYWORD_RESERVED_MASK)
494
+ rb_raise(rb_eArgError,
495
+ "winlog: keyword bits 48..63 are reserved by Microsoft "
496
+ "(mask 0x%016llX)", (unsigned long long)WINLOG_KEYWORD_RESERVED_MASK);
497
+ return kw;
498
+ }
499
+ }
500
+
501
+ /* opcode: Integer 0..255 (Ruby layer maps Symbols to Integers first). */
502
+ static UCHAR
503
+ opcode_to_uchar(VALUE vop)
504
+ {
505
+ long op = NUM2LONG(vop);
506
+ if (op < 0 || op > 255)
507
+ rb_raise(rb_eArgError, "winlog: opcode must be 0..255 (got %ld)", op);
508
+ return (UCHAR)op;
509
+ }
510
+
511
+ /* =================================================================
512
+ * Winlog::Provider — C methods
513
+ * =================================================================*/
514
+
515
+ /* Provider#_register(name) — construct + register the tld::Provider. Called by
516
+ * Ruby initialize AFTER name validation. Double-init guard (lithos pattern):
517
+ * raise Winlog::Error if already registered or closed (T11). Never raises on
518
+ * EventRegister failure: the tld handle is a benign no-op (E11). */
519
+ static VALUE
520
+ provider_register(VALUE self, VALUE name)
521
+ {
522
+ provider_t *pt = provider_get(self);
523
+ const char *cname;
524
+
525
+ if (pt->prov != NULL || pt->closed)
526
+ rb_raise(eError, "winlog: provider already registered");
527
+
528
+ /* name is a validated printable-ASCII String from the Ruby layer; a strict
529
+ * subset of UTF-8, safe for the char* (UTF-8) tld ctor. NUL-terminate via
530
+ * StringValueCStr (raises ArgumentError on an embedded NUL — defense in
531
+ * depth; the Ruby validator already forbids it). */
532
+ cname = StringValueCStr(name);
533
+
534
+ try {
535
+ pt->prov = new tld::Provider(cname);
536
+ } catch (...) {
537
+ pt->prov = NULL;
538
+ }
539
+ if (pt->prov == NULL)
540
+ rb_raise(rb_eNoMemError, "winlog: out of memory registering provider");
541
+
542
+ return self;
543
+ }
544
+
545
+ /* Provider#_write(level, event, fields, keyword, opcode, act_or_nil, rel_or_nil)
546
+ * UNGATED build+write, returns HRESULT Integer. Test/benchmark plumbing only
547
+ * (spec §3.3): EventWriteTransfer returns S_OK with no listener, so the full
548
+ * build path is exercisable without an elevated session. */
549
+ static VALUE
550
+ provider_write(VALUE self, VALUE vlevel, VALUE event, VALUE fields,
551
+ VALUE vkeyword, VALUE vopcode, VALUE vact, VALUE vrel)
552
+ {
553
+ provider_t *pt = provider_live(self);
554
+ UCHAR level = level_to_uchar(vlevel);
555
+ ULONGLONG keyword = keyword_to_ull(vkeyword);
556
+ UCHAR opcode = opcode_to_uchar(vopcode);
557
+ GUID act, rel;
558
+ GUID *pAct = NULL, *pRel = NULL;
559
+
560
+ if (!NIL_P(fields))
561
+ Check_Type(fields, T_HASH);
562
+
563
+ /* related: without activity: is meaningless (E18). The Ruby layer also
564
+ * guards this, but keep the C contract self-standing. */
565
+ if (!NIL_P(vrel) && NIL_P(vact))
566
+ rb_raise(rb_eArgError,
567
+ "winlog: related activity given without an activity id");
568
+
569
+ if (!NIL_P(vact)) {
570
+ if (!parse_guid(vact, &act))
571
+ rb_raise(rb_eArgError,
572
+ "winlog: activity must be a 36-char hyphenated GUID string");
573
+ pAct = &act;
574
+ }
575
+ if (!NIL_P(vrel)) {
576
+ if (!parse_guid(vrel, &rel))
577
+ rb_raise(rb_eArgError,
578
+ "winlog: related must be a 36-char hyphenated GUID string");
579
+ pRel = &rel;
580
+ }
581
+
582
+ return do_write(pt, event, fields, keyword, level, opcode, pAct, pRel);
583
+ }
584
+
585
+ /* Provider#_log(level, event, fields) — the full gated public path. Extracts
586
+ * control kwargs from +fields+ (rb_hash_lookup2 with a Qundef sentinel: cheap,
587
+ * no allocation, not iteration), validates them ALWAYS (E1), gates on
588
+ * IsEnabled(level, keyword), and only then builds+writes. The disabled return
589
+ * (Qfalse) happens BEFORE any field value is read (E1/E2 asymmetry). */
590
+ static VALUE
591
+ provider_log(VALUE self, VALUE vlevel, VALUE event, VALUE fields)
592
+ {
593
+ provider_t *pt = provider_live(self);
594
+ UCHAR level = level_to_uchar(vlevel); /* always validated */
595
+ VALUE vkw, vop, vact, vrel;
596
+ ULONGLONG keyword;
597
+ UCHAR opcode;
598
+ GUID act, rel;
599
+ GUID *pAct = NULL, *pRel = NULL;
600
+ VALUE hr;
601
+
602
+ Check_Type(fields, T_HASH);
603
+
604
+ /* Extract reserved control kwargs by exact Symbol key. String keys with the
605
+ * same text are NEVER control args (E21) — they stay as fields. */
606
+ vkw = rb_hash_lookup2(fields, ID2SYM(rb_intern("keyword")), Qundef);
607
+ vop = rb_hash_lookup2(fields, ID2SYM(rb_intern("opcode")), Qundef);
608
+ vact = rb_hash_lookup2(fields, ID2SYM(rb_intern("activity")), Qundef);
609
+ vrel = rb_hash_lookup2(fields, ID2SYM(rb_intern("related")), Qundef);
610
+
611
+ keyword = (vkw == Qundef) ? 0ULL : keyword_to_ull(vkw); /* always validated */
612
+
613
+ if (vop == Qundef) {
614
+ opcode = 0;
615
+ } else {
616
+ opcode = opcode_to_uchar(vop); /* Ruby layer maps Symbols first */
617
+ }
618
+
619
+ /* related: without activity: -> ArgumentError, even disabled (E18, E1). */
620
+ if (vrel != Qundef && (vact == Qundef || NIL_P(vact)))
621
+ rb_raise(rb_eArgError,
622
+ "winlog: related activity given without an activity id");
623
+
624
+ if (vact != Qundef && !NIL_P(vact)) {
625
+ if (!parse_guid(vact, &act))
626
+ rb_raise(rb_eArgError,
627
+ "winlog: activity must be a 36-char hyphenated GUID string");
628
+ pAct = &act;
629
+ }
630
+ if (vrel != Qundef && !NIL_P(vrel)) {
631
+ if (!parse_guid(vrel, &rel))
632
+ rb_raise(rb_eArgError,
633
+ "winlog: related must be a 36-char hyphenated GUID string");
634
+ pRel = &rel;
635
+ }
636
+
637
+ /* Validate the event name on EVERY call (E1/E6), enabled or not. This both
638
+ * matches the documented "control args always validated" contract and
639
+ * coerces from a clean frame before any C++ object exists. */
640
+ (void)coerce_name_utf8(event, "event name");
641
+
642
+ /* The gate. No system call; reads in-process enable state (E1, ~0.4 ns). */
643
+ if (!pt->prov->IsEnabled(level, keyword))
644
+ return Qfalse;
645
+
646
+ /* Enabled: build + write. Strip the control kwargs out so they are not
647
+ * emitted as fields. dup so we never mutate the caller's hash. */
648
+ {
649
+ VALUE eff = fields;
650
+ if (vkw != Qundef || vop != Qundef || vact != Qundef || vrel != Qundef) {
651
+ eff = rb_hash_dup(fields);
652
+ if (vkw != Qundef) rb_hash_delete(eff, ID2SYM(rb_intern("keyword")));
653
+ if (vop != Qundef) rb_hash_delete(eff, ID2SYM(rb_intern("opcode")));
654
+ if (vact != Qundef) rb_hash_delete(eff, ID2SYM(rb_intern("activity")));
655
+ if (vrel != Qundef) rb_hash_delete(eff, ID2SYM(rb_intern("related")));
656
+ }
657
+ hr = do_write(pt, event, eff, keyword, level, opcode, pAct, pRel);
658
+ }
659
+
660
+ /* hr success (S_OK == 0) -> true; any ETW drop -> false (never raise, E8). */
661
+ return (NUM2INT(hr) == 0) ? Qtrue : Qfalse;
662
+ }
663
+
664
+ /* Provider#_enabled(level_or_nil, keyword) — IsEnabled bridge. level nil means
665
+ * "enabled at any level" (uses IsEnabled() / level 0xFF probe semantics). */
666
+ static VALUE
667
+ provider_enabled(VALUE self, VALUE vlevel, VALUE vkeyword)
668
+ {
669
+ provider_t *pt = provider_live(self);
670
+ ULONGLONG keyword = keyword_to_ull(vkeyword);
671
+
672
+ if (NIL_P(vlevel)) {
673
+ /* "any level": enabled at all, with the keyword filter applied. Level 1
674
+ * (most restrictive that any session enabling implies) — but tld's
675
+ * IsEnabled(level,keyword) checks level < m_levelPlus1, so to mean "any
676
+ * level enabled" we probe with level 1 (a session enabling at any level
677
+ * L >= 1 sets m_levelPlus1 >= 2, so 1 < m_levelPlus1 holds). Combined
678
+ * with the keyword check this is the documented "enabled at any level"
679
+ * gate. */
680
+ UCHAR level = 1;
681
+ return pt->prov->IsEnabled(level, keyword) ? Qtrue : Qfalse;
682
+ } else {
683
+ UCHAR level = level_to_uchar(vlevel);
684
+ return pt->prov->IsEnabled(level, keyword) ? Qtrue : Qfalse;
685
+ }
686
+ }
687
+
688
+ /* Provider#guid -> 36-char lowercase hyphenated String (from tld Provider::Id,
689
+ * authoritative; equals Winlog.guid_for(name)). Set before Init runs, untouched
690
+ * by registration failure (E11). Raises Winlog::Closed after close. */
691
+ static VALUE
692
+ provider_guid(VALUE self)
693
+ {
694
+ provider_t *pt = provider_live(self);
695
+ return guid_to_rb(pt->prov->Id());
696
+ }
697
+
698
+ /* Provider#registered? -> true if EventRegister succeeded (E11). */
699
+ static VALUE
700
+ provider_registered_p(VALUE self)
701
+ {
702
+ provider_t *pt = provider_live(self);
703
+ return SUCCEEDED(pt->prov->InitializationResult()) ? Qtrue : Qfalse;
704
+ }
705
+
706
+ /* Provider#registration_result -> raw HRESULT Integer; 0 on success. */
707
+ static VALUE
708
+ provider_registration_result(VALUE self)
709
+ {
710
+ provider_t *pt = provider_live(self);
711
+ return INT2NUM((int)pt->prov->InitializationResult());
712
+ }
713
+
714
+ /* Provider#close -> nil. Idempotent (E20). Unregister + free the native
715
+ * provider now; safe to call again, on a never-registered object, etc. */
716
+ static VALUE
717
+ provider_close(VALUE self)
718
+ {
719
+ provider_t *pt = provider_get(self);
720
+ if (pt->prov) {
721
+ delete pt->prov; /* tld dtor: EventUnregister + HeapFree metadata */
722
+ pt->prov = NULL;
723
+ }
724
+ pt->closed = 1;
725
+ return Qnil;
726
+ }
727
+
728
+ /* Provider#closed? -> true/false. */
729
+ static VALUE
730
+ provider_closed_p(VALUE self)
731
+ {
732
+ provider_t *pt = provider_get(self);
733
+ return (pt->prov == NULL || pt->closed) ? Qtrue : Qfalse;
734
+ }
735
+
736
+ /* =================================================================
737
+ * Module function: Winlog.new_activity_id
738
+ * =================================================================*/
739
+
740
+ /* Winlog.new_activity_id -> 36-char lowercase hyphenated GUID String. Wraps
741
+ * EventActivityIdControl(CREATE_ID), which only GENERATES an id and never reads
742
+ * or writes the calling thread's implicit activity id (E17 fiber-safety).
743
+ * Raises Winlog::Error only if the OS call fails (practically never). */
744
+ static VALUE
745
+ winlog_new_activity_id(VALUE self)
746
+ {
747
+ GUID g;
748
+ ULONG status;
749
+
750
+ memset(&g, 0, sizeof g);
751
+ status = EventActivityIdControl(EVENT_ACTIVITY_CTRL_CREATE_ID, &g);
752
+ if (status != ERROR_SUCCESS)
753
+ rb_raise(eError,
754
+ "winlog: EventActivityIdControl(CREATE_ID) failed (error %lu)",
755
+ (unsigned long)status);
756
+ return guid_to_rb(g);
757
+ }
758
+
759
+ /* ----------------------------------------------------------------- Init ---- */
760
+
761
+ extern "C" void
762
+ Init_winlog(void)
763
+ {
764
+ mWinlog = rb_define_module("Winlog");
765
+
766
+ eError = rb_define_class_under(mWinlog, "Error", rb_eStandardError);
767
+ eClosed = rb_define_class_under(mWinlog, "Closed", eError);
768
+
769
+ cProvider = rb_define_class_under(mWinlog, "Provider", rb_cObject);
770
+ rb_define_alloc_func(cProvider, provider_alloc);
771
+
772
+ /* Private C bridges (underscore convention; hidden by the Ruby layer's
773
+ * `private`). The validated, keyword-aware public API lives in lib. */
774
+ rb_define_private_method(cProvider, "_register", RUBY_METHOD_FUNC(provider_register), 1);
775
+ rb_define_private_method(cProvider, "_log", RUBY_METHOD_FUNC(provider_log), 3);
776
+ rb_define_private_method(cProvider, "_write", RUBY_METHOD_FUNC(provider_write), 7);
777
+ rb_define_private_method(cProvider, "_enabled", RUBY_METHOD_FUNC(provider_enabled), 2);
778
+
779
+ /* Public read-only / simple methods exposed directly (spec §3.3). */
780
+ rb_define_method(cProvider, "guid", RUBY_METHOD_FUNC(provider_guid), 0);
781
+ rb_define_method(cProvider, "registered?", RUBY_METHOD_FUNC(provider_registered_p), 0);
782
+ rb_define_method(cProvider, "registration_result", RUBY_METHOD_FUNC(provider_registration_result), 0);
783
+ rb_define_method(cProvider, "close", RUBY_METHOD_FUNC(provider_close), 0);
784
+ rb_define_method(cProvider, "closed?", RUBY_METHOD_FUNC(provider_closed_p), 0);
785
+
786
+ rb_define_singleton_method(mWinlog, "new_activity_id",
787
+ RUBY_METHOD_FUNC(winlog_new_activity_id), 0);
788
+ }