diagnostics_message/
ffi.rs

1// Copyright 2025 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::error::MessageError;
6use crate::MonikerWithUrl;
7use bumpalo::collections::{String as BumpaloString, Vec as BumpaloVec};
8use bumpalo::Bump;
9use diagnostics_data::{ExtendedMoniker, Severity};
10use diagnostics_log_encoding::{Argument, Record, Value};
11use flyweights::FlyStr;
12use std::str;
13use zx::BootInstant;
14
15pub use crate::constants::*;
16
17struct ArchivistArguments<'a> {
18    builder: CPPLogMessageBuilder<'a>,
19    archivist_argument_count: usize,
20    record: Record<'a>,
21}
22
23#[cfg(fuchsia_api_level_less_than = "HEAD")]
24fn parse_archivist_args<'a>(
25    builder: CPPLogMessageBuilder<'a>,
26    input: Record<'a>,
27) -> Result<ArchivistArguments<'a>, MessageError> {
28    Ok(ArchivistArguments { builder, archivist_argument_count: 0, record: input })
29}
30
31#[cfg(fuchsia_api_level_at_least = "HEAD")]
32fn parse_archivist_args<'a>(
33    mut builder: CPPLogMessageBuilder<'a>,
34    input: Record<'a>,
35) -> Result<ArchivistArguments<'a>, MessageError> {
36    let mut has_moniker = false;
37    let mut archivist_argument_count = 0;
38    for argument in input.arguments.iter().rev() {
39        // If Archivist records are expected, they should always be at the end.
40        // If no Archivist records are expected, treat them as regular key-value-pairs.
41        match argument {
42            Argument::Other { value, name } => {
43                if name == fidl_fuchsia_diagnostics::MONIKER_ARG_NAME {
44                    if let Value::Text(moniker) = value {
45                        builder = builder.set_moniker(ExtendedMoniker::parse_str(moniker)?);
46                        archivist_argument_count += 1;
47                        has_moniker = true;
48                        continue;
49                    }
50                }
51                break;
52            }
53            _ => break,
54        }
55    }
56    if !has_moniker {
57        return Err(MessageError::MissingMoniker);
58    }
59    Ok(ArchivistArguments { builder, archivist_argument_count, record: input })
60}
61
62/// Array for FFI purposes between C++ and Rust.
63/// If len is 0, ptr is allowed to be nullptr,
64/// otherwise, ptr must be valid.
65#[repr(C)]
66pub struct CPPArray<T> {
67    /// Number of elements in the array
68    pub len: usize,
69    /// Pointer to the first element in the array,
70    /// may be null in the case of a 0 length array,
71    /// but is not guaranteed to always be null of
72    /// len is 0.
73    pub ptr: *const T,
74}
75
76impl CPPArray<u8> {
77    /// # Safety
78    ///
79    /// input must refer to a valid string, sized according to len.
80    /// A valid string consists of UTF-8 characters. The caller
81    /// is responsible for ensuring the byte sequence consists of valid UTF-8
82    /// characters.
83    ///
84    pub unsafe fn as_utf8_str(&self) -> &str {
85        std::str::from_utf8_unchecked(std::slice::from_raw_parts(self.ptr, self.len))
86    }
87}
88
89impl From<&str> for CPPArray<u8> {
90    fn from(value: &str) -> Self {
91        CPPArray { len: value.len(), ptr: value.as_ptr() }
92    }
93}
94
95impl From<Option<&str>> for CPPArray<u8> {
96    fn from(value: Option<&str>) -> Self {
97        if let Some(value) = value {
98            CPPArray { len: value.len(), ptr: value.as_ptr() }
99        } else {
100            CPPArray { len: 0, ptr: std::ptr::null() }
101        }
102    }
103}
104
105impl<T> From<&Vec<T>> for CPPArray<T> {
106    fn from(value: &Vec<T>) -> Self {
107        CPPArray { len: value.len(), ptr: value.as_ptr() }
108    }
109}
110
111/// Log message representation for FFI with C++
112#[repr(C)]
113pub struct LogMessage<'a> {
114    /// Severity of a log message.
115    severity: u8,
116    /// Tags in a log message, guaranteed to be non-null.
117    tags: CPPArray<CPPArray<u8>>,
118    /// Process ID from a LogMessage, or 0 if unknown
119    pid: u64,
120    /// Thread ID from a LogMessage, or 0 if unknown
121    tid: u64,
122    /// Number of dropped log messages.
123    dropped: u64,
124    /// The UTF-encoded log message, guaranteed to be valid UTF-8.
125    message: CPPArray<u8>,
126    /// Timestamp on the boot timeline of the log message,
127    /// in nanoseconds.
128    timestamp: i64,
129    /// Pointer to the builder is owned by this CPPLogMessage.
130    /// Dropping this CPPLogMessage will free the builder.
131    builder: *mut CPPLogMessageBuilder<'a>,
132}
133
134impl Drop for LogMessage<'_> {
135    fn drop(&mut self) {
136        unsafe {
137            // SAFETY: Rust guarantees destructors only run once in sound code.
138            // Initializing the CPPLogMessage requires the builder to be set
139            // to a valid pointer, so it is safe to drop the CPPLogMessageBuilder
140            // in the destructor to free resources on the Rust side of the FFI boundary.
141            std::ptr::drop_in_place(self.builder);
142        }
143    }
144}
145
146pub struct CPPLogMessageBuilder<'a> {
147    severity: u8,
148    tags: BumpaloVec<'a, BumpaloString<'a>>,
149    pid: Option<u64>,
150    tid: Option<u64>,
151    dropped: u64,
152    file: Option<String>,
153    line: Option<u64>,
154    moniker: Option<BumpaloString<'a>>,
155    message: Option<String>,
156    timestamp: i64,
157    kvps: BumpaloVec<'a, Argument<'a>>,
158    allocator: &'a Bump,
159}
160
161// Escape quotes in a string per the Feedback format
162fn escape_quotes(input: &str, output: &mut BumpaloString<'_>) {
163    for ch in input.chars() {
164        if ch == '\"' {
165            output.push('\\');
166        }
167        output.push(ch);
168    }
169}
170
171impl<'a> CPPLogMessageBuilder<'a> {
172    fn convert_string_vec(&self, strings: &[BumpaloString<'_>]) -> CPPArray<CPPArray<u8>> {
173        CPPArray {
174            len: strings.len(),
175            ptr: self
176                .allocator
177                .alloc_slice_fill_iter(strings.iter().map(|value| value.as_str().into()))
178                .as_ptr(),
179        }
180    }
181
182    fn set_raw_severity(mut self, raw_severity: u8) -> Self {
183        self.severity = raw_severity;
184        self
185    }
186
187    fn add_tag(mut self, tag: impl Into<String>) -> Self {
188        self.tags.push(BumpaloString::from_str_in(&tag.into(), self.allocator));
189        self
190    }
191
192    fn set_pid(mut self, pid: u64) -> Self {
193        self.pid = Some(pid);
194        self
195    }
196
197    fn set_tid(mut self, tid: u64) -> Self {
198        self.tid = Some(tid);
199        self
200    }
201
202    fn set_dropped(mut self, dropped: u64) -> Self {
203        self.dropped = dropped;
204        self
205    }
206
207    fn set_file(mut self, file: impl Into<String>) -> Self {
208        self.file = Some(file.into());
209        self
210    }
211
212    fn set_line(mut self, line: u64) -> Self {
213        self.line = Some(line);
214        self
215    }
216
217    fn set_message(mut self, msg: impl Into<String>) -> Self {
218        self.message = Some(msg.into());
219        self
220    }
221
222    fn add_kvp(mut self, kvp: Argument<'a>) -> Self {
223        self.kvps.push(kvp);
224        self
225    }
226    #[cfg(fuchsia_api_level_at_least = "HEAD")]
227    fn set_moniker(mut self, value: ExtendedMoniker) -> Self {
228        self.moniker = Some(bumpalo::format!(in self.allocator,"{}", value));
229        self
230    }
231    fn build(self) -> &'a mut LogMessage<'a> {
232        let allocator = self.allocator;
233        let builder = allocator.alloc(self);
234
235        // Format the message in accordance with the Feedback format
236        let msg_str = builder
237            .message
238            .as_ref()
239            .map(|value| bumpalo::format!(in &allocator,"{value}",))
240            .unwrap_or_else(|| BumpaloString::new_in(allocator));
241
242        let mut kvp_str = BumpaloString::new_in(allocator);
243        for kvp in &builder.kvps {
244            kvp_str = bumpalo::format!(in &allocator, "{kvp_str} {}=", kvp.name());
245            match kvp.value() {
246                Value::Text(value) => {
247                    kvp_str.push('"');
248                    escape_quotes(&value, &mut kvp_str);
249                    kvp_str.push('"');
250                }
251                Value::SignedInt(value) => {
252                    kvp_str.push_str(&bumpalo::format!(in &allocator, "{}",value))
253                }
254                Value::UnsignedInt(value) => {
255                    kvp_str.push_str(&bumpalo::format!(in &allocator, "{}",value))
256                }
257                Value::Floating(value) => {
258                    kvp_str.push_str(&bumpalo::format!(in &allocator, "{}",value))
259                }
260                Value::Boolean(value) => {
261                    if value {
262                        kvp_str.push_str("true");
263                    } else {
264                        kvp_str.push_str("false");
265                    }
266                }
267            }
268        }
269
270        let mut output = match (&builder.file, &builder.line) {
271            (Some(file), Some(line)) => {
272                let mut value = bumpalo::format!(in &allocator, "[{file}({line})]",);
273                if !msg_str.is_empty() {
274                    value.push(' ');
275                }
276                value
277            }
278            _ => BumpaloString::new_in(allocator),
279        };
280
281        output.push_str(&msg_str);
282        output.push_str(&kvp_str);
283
284        if let Some(moniker) = &builder.moniker {
285            let component_name = moniker.split("/").last();
286            if let Some(component_name) = component_name {
287                if !builder.tags.iter().any(|value| value.as_str() == component_name) {
288                    builder.tags.insert(0, bumpalo::format!(in &allocator, "{}", component_name));
289                }
290            }
291        }
292
293        let log_message = LogMessage {
294            builder,
295            severity: builder.severity,
296            dropped: builder.dropped,
297            tags: builder.convert_string_vec(&builder.tags),
298            pid: builder.pid.unwrap_or(0),
299            tid: builder.tid.unwrap_or(0),
300            message: output.as_str().into(),
301            timestamp: builder.timestamp,
302        };
303
304        allocator.alloc(log_message)
305    }
306}
307
308struct CPPLogMessageBuilderBuilder<'a>(&'a Bump);
309
310impl<'a> CPPLogMessageBuilderBuilder<'a> {
311    fn configure(
312        self,
313        _component_url: Option<FlyStr>,
314        moniker: Option<ExtendedMoniker>,
315        severity: Severity,
316        timestamp: BootInstant,
317    ) -> Result<CPPLogMessageBuilder<'a>, MessageError> {
318        Ok(CPPLogMessageBuilder {
319            severity: severity as u8,
320            tags: BumpaloVec::new_in(self.0),
321            pid: None,
322            tid: None,
323            dropped: 0,
324            file: None,
325            timestamp: timestamp.into_nanos(),
326            line: None,
327            allocator: self.0,
328            kvps: BumpaloVec::new_in(self.0),
329            moniker: moniker.map(|value| bumpalo::format!(in self.0,"{}", value)),
330            message: None,
331        })
332    }
333}
334
335fn build_logs_data<'a>(
336    input: Record<'a>,
337    source: Option<MonikerWithUrl>,
338    allocator: &'a Bump,
339    expect_extended_attribution: bool,
340) -> Result<CPPLogMessageBuilder<'a>, MessageError> {
341    let builder = CPPLogMessageBuilderBuilder(allocator);
342    let (raw_severity, severity) = Severity::parse_exact(input.severity);
343    let (maybe_moniker, maybe_url) =
344        source.map(|value| (Some(value.moniker), Some(value.url))).unwrap_or((None, None));
345    let mut builder = builder.configure(maybe_url, maybe_moniker, severity, input.timestamp)?;
346    if let Some(raw_severity) = raw_severity {
347        builder = builder.set_raw_severity(raw_severity);
348    }
349    let (archivist_argument_count, input) = if !expect_extended_attribution {
350        (0, input)
351    } else {
352        let arguments = parse_archivist_args(builder, input)?;
353        builder = arguments.builder;
354        (arguments.archivist_argument_count, arguments.record)
355    };
356    let input_argument_len = input.arguments.len();
357    for argument in input.arguments.into_iter().take(input_argument_len - archivist_argument_count)
358    {
359        match argument {
360            Argument::Tag(tag) => {
361                builder = builder.add_tag(tag.as_ref());
362            }
363            Argument::Pid(pid) => {
364                builder = builder.set_pid(pid.raw_koid());
365            }
366            Argument::Tid(tid) => {
367                builder = builder.set_tid(tid.raw_koid());
368            }
369            Argument::Dropped(dropped) => {
370                builder = builder.set_dropped(dropped);
371            }
372            Argument::File(file) => {
373                builder = builder.set_file(file.as_ref());
374            }
375            Argument::Line(line) => {
376                builder = builder.set_line(line);
377            }
378            Argument::Message(msg) => {
379                builder = builder.set_message(msg.as_ref());
380            }
381            Argument::Other { value: _, name: _ } => builder = builder.add_kvp(argument),
382        }
383    }
384
385    Ok(builder)
386}
387
388/// Constructs a `CPPLogsMessage` from the provided bytes, assuming the bytes
389/// are in the format specified as in the [log encoding], and come from
390///
391/// an Archivist LogStream with moniker, URL, and dropped logs output enabled.
392/// [log encoding] https://fuchsia.dev/fuchsia-src/development/logs/encodings
393pub fn ffi_from_extended_record<'a>(
394    bytes: &'a [u8],
395    allocator: &'a Bump,
396    source: Option<MonikerWithUrl>,
397    expect_extended_attribution: bool,
398) -> Result<(&'a mut LogMessage<'a>, &'a [u8]), MessageError> {
399    let (input, remaining) = diagnostics_log_encoding::parse::parse_record(bytes)?;
400    let record = build_logs_data(input, source, allocator, expect_extended_attribution)?.build();
401    Ok((record, remaining))
402}