1212#include < cerrno>
1313#include < chrono>
1414#include < cstdio>
15+ #include < cstring>
1516#include < ctime>
1617#include < iomanip>
1718#include < iostream>
@@ -156,10 +157,13 @@ struct LogSink {
156157 is_good(true )
157158 {}
158159
159- void write (const std::string &line) {
160+ // Raw (data,len) overload — lets callers emit a fixed char buffer without
161+ // constructing an intermediate std::string (keeps the log hot path allocation-free).
162+ void write (const char *data, std::size_t len) {
160163 std::lock_guard<std::mutex> lk (mtx);
161164 if (is_good) {
162- *stream << line << ' \n ' ;
165+ stream->write (data, static_cast <std::streamsize>(len));
166+ stream->put (' \n ' );
163167 // stream->fail() is a single rdstate() bitmask read — negligible cost.
164168 // Once a failure is detected, is_good=false so all future calls return
165169 // immediately at the guard above, paying zero additional cost.
@@ -172,6 +176,8 @@ struct LogSink {
172176 }
173177 }
174178
179+ void write (const std::string &line) { write (line.data (), line.size ()); }
180+
175181 void flush () {
176182 std::lock_guard<std::mutex> lk (mtx);
177183 if (stream) {
@@ -235,10 +241,13 @@ void ZeLogger::flush() {
235241// %^%l%$ — level label (with color when tty)
236242// %v — message
237243//
238- // formatLine writes into an existing string (passed by ref) to avoid
239- // a return-by-value heap allocation on every log call.
244+ // formatLine writes into a caller-provided fixed char buffer (snprintf-style)
245+ // and returns the total length the line needs. It keeps no owning static state
246+ // (POD thread_local buffers only), so it is allocation-free on the hot path and
247+ // safe to call during exit-time teardown.
240248// ---------------------------------------------------------------------------
241- void ZeLogger::formatLine (LogLevel msg_level, const std::string &msg, std::string &out) {
249+ std::size_t ZeLogger::formatLine (LogLevel msg_level, const std::string &msg,
250+ char *out, std::size_t cap) {
242251 // Build timestamp: YYYY-MM-DD HH:MM:SS.mmm
243252 auto now = std::chrono::system_clock::now ();
244253 auto now_t = std::chrono::system_clock::to_time_t (now);
@@ -259,22 +268,50 @@ void ZeLogger::formatLine(LogLevel msg_level, const std::string &msg, std::strin
259268 std::snprintf (ts_full, sizeof (ts_full), " %s.%03lld" , ts,
260269 static_cast <long long >(ms.count ()));
261270
262- // Thread id — computed once per thread, stored in a thread_local local
263- // static inside this non-inline, non-template .cpp function.
264- // This produces STB_LOCAL linkage (not STB_GNU_UNIQUE), so dlclose() is unaffected.
265- static thread_local const std::string tid_str = [](){
271+ // Thread id — computed once per thread into a POD thread_local char buffer.
272+ // POD storage has a trivial destructor (nothing runs at thread/exit teardown),
273+ // so it cannot become a use-after-free when logging happens during process exit;
274+ // and as a function-local static in this non-inline, non-template .cpp function
275+ // it has STB_LOCAL linkage (not STB_GNU_UNIQUE), so dlclose() is unaffected.
276+ static thread_local char tid_buf[32 ];
277+ static thread_local std::size_t tid_len = 0 ;
278+ if (tid_len == 0 ) {
266279 std::ostringstream ss;
267280 ss << std::this_thread::get_id ();
268- return ss.str ();
269- }();
281+ const std::string s = ss.str ();
282+ tid_len = s.size () < sizeof (tid_buf) ? s.size () : sizeof (tid_buf);
283+ std::memcpy (tid_buf, s.data (), tid_len);
284+ }
285+
286+ char pid_buf[24 ];
287+ int pid_written = std::snprintf (pid_buf, sizeof (pid_buf), " %lld" ,
288+ static_cast <long long >(GET_PID ()));
289+ if (pid_written < 0 ) pid_written = 0 ;
290+ const std::size_t pid_len =
291+ static_cast <std::size_t >(pid_written) < sizeof (pid_buf)
292+ ? static_cast <std::size_t >(pid_written)
293+ : sizeof (pid_buf) - 1 ;
270294
271295 const char *label = levelLabel (msg_level);
272296 const char *color = _sink->color_enabled ? levelColor (msg_level) : " " ;
273297 const char *reset = _sink->color_enabled ? AnsiColor::reset () : " " ;
274298
275- // Write directly into the caller-provided string — no extra allocation.
276- out.clear ();
277- out.reserve (_pattern.size () + msg.size () + 64 );
299+ // Bounded append into the caller buffer. `len` always tracks the TOTAL bytes
300+ // the full line needs (so the caller can detect overflow), but we never write
301+ // past `cap`. No heap allocation on this path.
302+ std::size_t len = 0 ;
303+ auto put = [&](const char *s, std::size_t n) {
304+ if (len < cap) {
305+ std::size_t avail = cap - len;
306+ std::memcpy (out + len, s, n < avail ? n : avail);
307+ }
308+ len += n;
309+ };
310+ auto put_cstr = [&](const char *s) { put (s, std::strlen (s)); };
311+ auto put_ch = [&](char c) {
312+ if (len < cap) out[len] = c;
313+ len += 1 ;
314+ };
278315
279316 bool color_span_open = false ;
280317
@@ -285,64 +322,77 @@ void ZeLogger::formatLine(LogLevel msg_level, const std::string &msg, std::strin
285322 case ' Y' : {
286323 const char *seq = " %Y-%m-%d %H:%M:%S.%e" ;
287324 if (_pattern.compare (i, 20 , seq) == 0 ) {
288- out += ts_full;
325+ put_cstr ( ts_full) ;
289326 i += 19 ;
290327 } else {
291- out += ' %' ;
292- out += tok;
328+ put_ch ( ' %' ) ;
329+ put_ch ( tok) ;
293330 ++i;
294331 }
295332 break ;
296333 }
297334 case ' t' :
298- out += tid_str ;
335+ put (tid_buf, tid_len) ;
299336 ++i;
300337 break ;
301338 case ' P' :
302- out += std::to_string ( GET_PID () );
339+ put (pid_buf, pid_len );
303340 ++i;
304341 break ;
305342 case ' ^' :
306- out += color;
343+ put_cstr ( color) ;
307344 color_span_open = true ;
308345 ++i;
309346 break ;
310347 case ' l' :
311- out += label;
348+ put_cstr ( label) ;
312349 ++i;
313350 break ;
314351 case ' $' :
315- out += reset;
352+ put_cstr ( reset) ;
316353 color_span_open = false ;
317354 ++i;
318355 break ;
319356 case ' v' :
320- out += msg;
357+ put (msg. data (), msg. size ()) ;
321358 ++i;
322359 break ;
323360 default :
324- out += ' %' ;
361+ put_ch ( ' %' ) ;
325362 break ;
326363 }
327364 } else {
328- out += _pattern[i];
365+ put_ch ( _pattern[i]) ;
329366 }
330367 }
331368
332369 if (color_span_open) {
333- out += reset;
370+ put_cstr ( reset) ;
334371 }
372+
373+ return len;
335374}
336375
337376void ZeLogger::write (LogLevel msg_level, const std::string &msg) {
338377 if (!_sink || msg_level < _level) {
339378 return ;
340379 }
341- // Reuse a thread_local buffer to avoid a heap allocation per log call.
380+ // POD thread_local buffer — trivial destructor, freed with the thread, no heap
381+ // leak. A log call can arrive during exit-time teardown (e.g. SYCL shutdown →
382+ // validation-layer DDI → log) AFTER a non-trivial thread_local (std::string)
383+ // would have been destroyed; a POD char buffer is safe to use at any time.
342384 // STB_LOCAL linkage (non-inline, non-template .cpp function) — safe for dlclose.
343- static thread_local std::string line_buf;
344- formatLine (msg_level, msg, line_buf);
345- _sink->write (line_buf);
385+ static thread_local char line_buf[2048 ];
386+ const std::size_t n = formatLine (msg_level, msg, line_buf, sizeof (line_buf));
387+ if (n <= sizeof (line_buf)) {
388+ _sink->write (line_buf, n); // hot path: zero allocation
389+ } else {
390+ // Rare oversized line: one allocation, same output (no truncation).
391+ // Line length is deterministic for a given message, so a single retry fits.
392+ std::string big (n, ' \0 ' );
393+ const std::size_t n2 = formatLine (msg_level, msg, &big[0 ], big.size ());
394+ _sink->write (big.data (), n2 < big.size () ? n2 : big.size ());
395+ }
346396}
347397
348398void ZeLogger::trace (const std::string &msg) { write (LogLevel::trace, msg); }
0 commit comments