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