glean_ffi/
lib.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5#![deny(rustdoc::broken_intra_doc_links)]
6
7use std::convert::TryFrom;
8use std::ffi::CStr;
9use std::os::raw::c_char;
10use std::panic::UnwindSafe;
11use std::path::PathBuf;
12
13use ffi_support::{define_string_destructor, ConcurrentHandleMap, FfiStr, IntoFfi};
14
15#[cfg(all(not(target_os = "android"), not(target_os = "ios")))]
16use once_cell::sync::OnceCell;
17
18pub use glean_core::metrics::MemoryUnit;
19pub use glean_core::metrics::TimeUnit;
20pub use glean_core::upload::ffi_upload_result::*;
21use glean_core::Glean;
22pub use glean_core::Lifetime;
23
24mod macros;
25
26mod boolean;
27pub mod byte_buffer;
28mod counter;
29mod custom_distribution;
30mod datetime;
31mod event;
32mod ffi_string_ext;
33mod from_raw;
34mod handlemap_ext;
35mod jwe;
36mod labeled;
37mod memory_distribution;
38pub mod ping_type;
39mod quantity;
40mod string;
41mod string_list;
42mod timespan;
43mod timing_distribution;
44pub mod upload;
45mod url;
46mod uuid;
47
48#[cfg(all(not(target_os = "android"), not(target_os = "ios")))]
49mod fd_logger;
50
51#[cfg(unix)]
52#[macro_use]
53mod weak;
54
55use ffi_string_ext::FallibleToString;
56use from_raw::*;
57use handlemap_ext::HandleMapExtension;
58use ping_type::PING_TYPES;
59use upload::FfiPingUploadTask;
60
61/// Execute the callback with a reference to the Glean singleton, returning a `Result`.
62///
63/// The callback returns a `Result<T, E>` while:
64///
65/// - Catching panics, and logging them.
66/// - Converting `T` to a C-compatible type using [`IntoFfi`].
67/// - Logging `E` and returning a default value.
68pub(crate) fn with_glean<R, F>(callback: F) -> R::Value
69where
70    F: UnwindSafe + FnOnce(&Glean) -> Result<R, glean_core::Error>,
71    R: IntoFfi,
72{
73    let mut error = ffi_support::ExternError::success();
74    let res =
75        ffi_support::abort_on_panic::call_with_result(
76            &mut error,
77            || match glean_core::global_glean() {
78                Some(glean) => {
79                    let glean = glean.lock().unwrap();
80                    callback(&glean)
81                }
82                None => Err(glean_core::Error::not_initialized()),
83            },
84        );
85    handlemap_ext::log_if_error(error);
86    res
87}
88
89/// Execute the callback with a mutable reference to the Glean singleton, returning a `Result`.
90///
91/// The callback returns a `Result<T, E>` while:
92///
93/// - Catching panics, and logging them.
94/// - Converting `T` to a C-compatible type using [`IntoFfi`].
95/// - Logging `E` and returning a default value.
96pub(crate) fn with_glean_mut<R, F>(callback: F) -> R::Value
97where
98    F: UnwindSafe + FnOnce(&mut Glean) -> Result<R, glean_core::Error>,
99    R: IntoFfi,
100{
101    let mut error = ffi_support::ExternError::success();
102    let res =
103        ffi_support::abort_on_panic::call_with_result(
104            &mut error,
105            || match glean_core::global_glean() {
106                Some(glean) => {
107                    let mut glean = glean.lock().unwrap();
108                    callback(&mut glean)
109                }
110                None => Err(glean_core::Error::not_initialized()),
111            },
112        );
113    handlemap_ext::log_if_error(error);
114    res
115}
116
117/// Execute the callback with a reference to the Glean singleton, returning a value.
118///
119/// The callback returns a value while:
120///
121/// - Catching panics, and logging them.
122/// - Converting the returned value to a C-compatible type using [`IntoFfi`].
123pub(crate) fn with_glean_value<R, F>(callback: F) -> R::Value
124where
125    F: UnwindSafe + FnOnce(&Glean) -> R,
126    R: IntoFfi,
127{
128    with_glean(|glean| Ok(callback(glean)))
129}
130
131/// Execute the callback with a mutable reference to the Glean singleton, returning a value.
132///
133/// The callback returns a value while:
134///
135/// - Catching panics, and logging them.
136/// - Converting the returned value to a C-compatible type using [`IntoFfi`].
137pub(crate) fn with_glean_value_mut<R, F>(callback: F) -> R::Value
138where
139    F: UnwindSafe + FnOnce(&mut Glean) -> R,
140    R: IntoFfi,
141{
142    with_glean_mut(|glean| Ok(callback(glean)))
143}
144
145/// Initialize the logging system based on the target platform. This ensures
146/// that logging is shown when executing the Glean SDK unit tests.
147#[no_mangle]
148pub extern "C" fn glean_enable_logging() {
149    #[cfg(target_os = "android")]
150    {
151        let _ = std::panic::catch_unwind(|| {
152            let filter = android_logger::FilterBuilder::new()
153                .filter_module("glean_ffi", log::LevelFilter::Debug)
154                .filter_module("glean_core", log::LevelFilter::Debug)
155                .filter_module("glean", log::LevelFilter::Debug)
156                .build();
157            android_logger::init_once(
158                android_logger::Config::default()
159                    .with_min_level(log::Level::Debug)
160                    .with_filter(filter)
161                    .with_tag("libglean_ffi"),
162            );
163            log::trace!("Android logging should be hooked up!")
164        });
165    }
166
167    // On iOS enable logging with a level filter.
168    #[cfg(target_os = "ios")]
169    {
170        // Debug logging in debug mode.
171        // (Note: `debug_assertions` is the next best thing to determine if this is a debug build)
172        #[cfg(debug_assertions)]
173        let level = log::LevelFilter::Debug;
174        #[cfg(not(debug_assertions))]
175        let level = log::LevelFilter::Info;
176
177        let logger = oslog::OsLogger::new("org.mozilla.glean").level_filter(level);
178
179        match logger.init() {
180            Ok(_) => log::trace!("os_log should be hooked up!"),
181            // Please note that this is only expected to fail during unit tests,
182            // where the logger might have already been initialized by a previous
183            // test. So it's fine to print with the "logger".
184            Err(_) => log::warn!("os_log was already initialized"),
185        };
186    }
187
188    // Make sure logging does something on non Android platforms as well. Use
189    // the RUST_LOG environment variable to set the desired log level, e.g.
190    // setting RUST_LOG=debug sets the log level to debug.
191    #[cfg(all(not(target_os = "android"), not(target_os = "ios")))]
192    {
193        match env_logger::try_init() {
194            Ok(_) => log::trace!("stdout logging should be hooked up!"),
195            // Please note that this is only expected to fail during unit tests,
196            // where the logger might have already been initialized by a previous
197            // test. So it's fine to print with the "logger".
198            Err(_) => log::warn!("stdout logging was already initialized"),
199        };
200    }
201}
202
203#[cfg(all(not(target_os = "android"), not(target_os = "ios")))]
204static FD_LOGGER: OnceCell<fd_logger::FdLogger> = OnceCell::new();
205
206/// Initialize the logging system to send JSON messages to a file descriptor
207/// (Unix) or file handle (Windows).
208///
209/// Not available on Android and iOS.
210///
211/// `fd` is a writable file descriptor (on Unix) or file handle (on Windows).
212///
213/// # Safety
214/// Unsafe because the fd u64 passed in will be interpreted as either a file
215/// descriptor (Unix) or file handle (Windows) without any checking.
216#[cfg(all(not(target_os = "android"), not(target_os = "ios")))]
217#[no_mangle]
218pub unsafe extern "C" fn glean_enable_logging_to_fd(fd: u64) {
219    // Set up logging to a file descriptor/handle. For this usage, the
220    // language binding should setup a pipe and pass in the descriptor to
221    // the writing side of the pipe as the `fd` parameter. Log messages are
222    // written as JSON to the file descriptor.
223    if FD_LOGGER.set(fd_logger::FdLogger::new(fd)).is_ok() {
224        // Set the level so everything goes through to the language
225        // binding side where it will be filtered by the language
226        // binding's logging system.
227        if log::set_logger(FD_LOGGER.get().unwrap()).is_ok() {
228            log::set_max_level(log::LevelFilter::Debug);
229        }
230    }
231}
232
233/// Configuration over FFI.
234///
235/// **CAUTION**: This must match _exactly_ the definition on the Kotlin side.
236/// If this side is changed, the Kotlin side need to be changed, too.
237#[repr(C)]
238pub struct FfiConfiguration<'a> {
239    pub data_dir: FfiStr<'a>,
240    pub package_name: FfiStr<'a>,
241    pub language_binding_name: FfiStr<'a>,
242    pub upload_enabled: u8,
243    pub max_events: Option<&'a i32>,
244    pub delay_ping_lifetime_io: u8,
245}
246
247/// Convert the FFI-compatible configuration object into the proper Rust configuration object.
248impl TryFrom<&FfiConfiguration<'_>> for glean_core::Configuration {
249    type Error = glean_core::Error;
250
251    fn try_from(cfg: &FfiConfiguration) -> Result<Self, Self::Error> {
252        let data_path = cfg.data_dir.to_string_fallible()?;
253        let data_path = PathBuf::from(data_path);
254        let application_id = cfg.package_name.to_string_fallible()?;
255        let language_binding_name = cfg.language_binding_name.to_string_fallible()?;
256        let upload_enabled = cfg.upload_enabled != 0;
257        let max_events = cfg.max_events.filter(|&&i| i >= 0).map(|m| *m as usize);
258        let delay_ping_lifetime_io = cfg.delay_ping_lifetime_io != 0;
259        let app_build = "Unknown".to_string();
260        let use_core_mps = false;
261
262        Ok(Self {
263            upload_enabled,
264            data_path,
265            application_id,
266            language_binding_name,
267            max_events,
268            delay_ping_lifetime_io,
269            app_build,
270            use_core_mps,
271        })
272    }
273}
274
275/// # Safety
276///
277/// A valid and non-null configuration object is required for this function.
278#[no_mangle]
279pub unsafe extern "C" fn glean_initialize(cfg: *const FfiConfiguration) -> u8 {
280    assert!(!cfg.is_null());
281
282    handlemap_ext::handle_result(|| {
283        // We can create a reference to the FfiConfiguration struct:
284        // 1. We did a null check
285        // 2. We're not holding on to it beyond this function
286        //    and we copy out all data when needed.
287        let glean_cfg = glean_core::Configuration::try_from(&*cfg)?;
288        let glean = Glean::new(glean_cfg)?;
289        glean_core::setup_glean(glean)?;
290        log::info!("Glean initialized");
291        Ok(true)
292    })
293}
294
295#[no_mangle]
296pub extern "C" fn glean_on_ready_to_submit_pings() -> u8 {
297    with_glean_value(|glean| glean.on_ready_to_submit_pings())
298}
299
300#[no_mangle]
301pub extern "C" fn glean_is_upload_enabled() -> u8 {
302    with_glean_value(|glean| glean.is_upload_enabled())
303}
304
305#[no_mangle]
306pub extern "C" fn glean_set_upload_enabled(flag: u8) {
307    with_glean_value_mut(|glean| glean.set_upload_enabled(flag != 0));
308    // The return value of set_upload_enabled is an implementation detail
309    // that isn't exposed over FFI.
310}
311
312#[no_mangle]
313pub extern "C" fn glean_submit_ping_by_name(ping_name: FfiStr, reason: FfiStr) -> u8 {
314    with_glean(|glean| {
315        Ok(glean.submit_ping_by_name(&ping_name.to_string_fallible()?, reason.as_opt_str()))
316    })
317}
318
319#[no_mangle]
320pub extern "C" fn glean_ping_collect(ping_type_handle: u64, reason: FfiStr) -> *mut c_char {
321    with_glean_value(|glean| {
322        PING_TYPES.call_infallible(ping_type_handle, |ping_type| {
323            let ping_maker = glean_core::ping::PingMaker::new();
324            let data = ping_maker
325                .collect_string(glean, ping_type, reason.as_opt_str())
326                .unwrap_or_else(|| String::from(""));
327            log::info!("Ping({}): {}", ping_type.name.as_str(), data);
328            data
329        })
330    })
331}
332
333#[no_mangle]
334pub extern "C" fn glean_set_experiment_active(
335    experiment_id: FfiStr,
336    branch: FfiStr,
337    extra_keys: RawStringArray,
338    extra_values: RawStringArray,
339    extra_len: i32,
340) {
341    with_glean(|glean| {
342        let experiment_id = experiment_id.to_string_fallible()?;
343        let branch = branch.to_string_fallible()?;
344        let extra = from_raw_string_array_and_string_array(extra_keys, extra_values, extra_len)?;
345
346        glean.set_experiment_active(experiment_id, branch, extra);
347        Ok(())
348    })
349}
350
351#[no_mangle]
352pub extern "C" fn glean_set_experiment_inactive(experiment_id: FfiStr) {
353    with_glean(|glean| {
354        let experiment_id = experiment_id.to_string_fallible()?;
355        glean.set_experiment_inactive(experiment_id);
356        Ok(())
357    })
358}
359
360#[no_mangle]
361pub extern "C" fn glean_experiment_test_is_active(experiment_id: FfiStr) -> u8 {
362    with_glean(|glean| {
363        let experiment_id = experiment_id.to_string_fallible()?;
364        Ok(glean.test_is_experiment_active(experiment_id))
365    })
366}
367
368#[no_mangle]
369pub extern "C" fn glean_experiment_test_get_data(experiment_id: FfiStr) -> *mut c_char {
370    with_glean(|glean| {
371        let experiment_id = experiment_id.to_string_fallible()?;
372        Ok(glean.test_get_experiment_data_as_json(experiment_id))
373    })
374}
375
376#[no_mangle]
377pub extern "C" fn glean_clear_application_lifetime_metrics() {
378    with_glean_value(|glean| glean.clear_application_lifetime_metrics());
379}
380
381/// Try to unblock the RLB dispatcher to start processing queued tasks.
382///
383/// **Note**: glean-core does not have its own dispatcher at the moment.
384/// This tries to detect the RLB and, if loaded, instructs the RLB dispatcher to flush.
385/// This allows the usage of both the RLB and other language bindings (e.g. Kotlin/Swift)
386/// within the same application.
387#[no_mangle]
388pub extern "C" fn glean_flush_rlb_dispatcher() {
389    #[cfg(unix)]
390    #[allow(non_upper_case_globals)]
391    {
392        weak!(fn rlb_flush_dispatcher() -> ());
393
394        if let Some(f) = rlb_flush_dispatcher.get() {
395            // SAFETY:
396            //
397            // We did a dynamic lookup for this symbol.
398            // This is only called if we found it.
399            // We don't pass any data and don't read any return value, thus no data we directly
400            // depend on will be corruptable.
401            unsafe {
402                f();
403            }
404        } else {
405            log::info!("No RLB symbol found. Not trying to flush the RLB dispatcher.");
406        }
407    }
408}
409
410#[no_mangle]
411pub extern "C" fn glean_set_dirty_flag(flag: u8) {
412    with_glean_value_mut(|glean| glean.set_dirty_flag(flag != 0));
413}
414
415#[no_mangle]
416pub extern "C" fn glean_is_dirty_flag_set() -> u8 {
417    with_glean_value(|glean| glean.is_dirty_flag_set())
418}
419
420#[no_mangle]
421pub extern "C" fn glean_handle_client_active() {
422    with_glean_value_mut(|glean| glean.handle_client_active());
423}
424
425#[no_mangle]
426pub extern "C" fn glean_handle_client_inactive() {
427    with_glean_value_mut(|glean| glean.handle_client_inactive());
428}
429
430#[no_mangle]
431pub extern "C" fn glean_test_clear_all_stores() {
432    with_glean_value(|glean| glean.test_clear_all_stores())
433}
434
435#[no_mangle]
436pub extern "C" fn glean_destroy_glean() {
437    with_glean_value_mut(|glean| glean.destroy_db())
438}
439
440#[no_mangle]
441pub extern "C" fn glean_is_first_run() -> u8 {
442    with_glean_value(|glean| glean.is_first_run())
443}
444
445// Unfortunately, the way we use CFFI in Python ("out-of-line", "ABI mode") does not
446// allow return values to be `union`s, so we need to use an output parameter instead of
447// a return value to get the task. The output data will be consumed and freed by the
448// `glean_process_ping_upload_response` below.
449//
450// Arguments:
451//
452// * `result`: the object the output task will be written to.
453#[no_mangle]
454pub extern "C" fn glean_get_upload_task(result: *mut FfiPingUploadTask) {
455    with_glean_value(|glean| {
456        let ffi_task = FfiPingUploadTask::from(glean.get_upload_task());
457        unsafe {
458            std::ptr::write(result, ffi_task);
459        }
460    });
461}
462
463/// Process and free a `FfiPingUploadTask`.
464///
465/// We need to pass the whole task instead of only the document id,
466/// so that we can free the strings properly on Drop.
467///
468/// After return the `task` should not be used further by the caller.
469///
470/// # Safety
471///
472/// A valid and non-null upload task object is required for this function.
473#[no_mangle]
474pub unsafe extern "C" fn glean_process_ping_upload_response(
475    task: *mut FfiPingUploadTask,
476    status: u32,
477) {
478    // Safety:
479    // * We null-check the passed task before dereferencing.
480    // * We replace data behind the pointer with another valid variant.
481    // * We gracefully handle invalid data in strings.
482    if task.is_null() {
483        return;
484    }
485
486    // Take out task and replace with valid value.
487    // This value should never be read again on the FFI side,
488    // but as it controls the memory, we put something valid in place, just in case.
489    let task = std::ptr::replace(task, FfiPingUploadTask::Done);
490
491    with_glean(|glean| {
492        if let FfiPingUploadTask::Upload { document_id, .. } = task {
493            assert!(!document_id.is_null());
494            let document_id_str = CStr::from_ptr(document_id)
495                .to_str()
496                .map_err(|_| glean_core::Error::utf8_error())?;
497            glean.process_ping_upload_response(document_id_str, status.into());
498        };
499        Ok(())
500    });
501}
502
503/// # Safety
504///
505/// A valid and non-null configuration object is required for this function.
506#[no_mangle]
507pub unsafe extern "C" fn glean_initialize_for_subprocess(cfg: *const FfiConfiguration) -> u8 {
508    assert!(!cfg.is_null());
509
510    handlemap_ext::handle_result(|| {
511        // We can create a reference to the FfiConfiguration struct:
512        // 1. We did a null check
513        // 2. We're not holding on to it beyond this function
514        //    and we copy out all data when needed.
515        let glean_cfg = glean_core::Configuration::try_from(&*cfg)?;
516        let glean = Glean::new_for_subprocess(&glean_cfg, true)?;
517        glean_core::setup_glean(glean)?;
518        log::info!("Glean initialized for subprocess");
519        Ok(true)
520    })
521}
522
523#[no_mangle]
524pub extern "C" fn glean_set_debug_view_tag(tag: FfiStr) -> u8 {
525    with_glean_mut(|glean| {
526        let tag = tag.to_string_fallible()?;
527        Ok(glean.set_debug_view_tag(&tag))
528    })
529}
530
531#[no_mangle]
532pub extern "C" fn glean_set_log_pings(value: u8) {
533    with_glean_mut(|glean| Ok(glean.set_log_pings(value != 0)));
534}
535
536#[no_mangle]
537pub extern "C" fn glean_set_source_tags(raw_tags: RawStringArray, tags_count: i32) -> u8 {
538    with_glean_mut(|glean| {
539        let tags = from_raw_string_array(raw_tags, tags_count)?;
540        Ok(glean.set_source_tags(tags))
541    })
542}
543
544#[no_mangle]
545pub extern "C" fn glean_get_timestamp_ms() -> u64 {
546    glean_core::get_timestamp_ms()
547}
548
549define_string_destructor!(glean_str_free);