diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6f09f1c4f..b5be94efd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -148,7 +148,7 @@ jobs: env: RUSTFLAGS: "${{ matrix.flags }}" run: | - cargo run --bin release --features profiling,telemetry,data-pipeline,symbolizer,crashtracker,library-config,log --release -- --out $LIBDD_OUTPUT_FOLDER + cargo run --bin release --features profiling,telemetry,data-pipeline,symbolizer,crashtracker,library-config,log,ddsketch --release -- --out $LIBDD_OUTPUT_FOLDER - name: 'Publish libdatadog' uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # 4.6.1 diff --git a/Cargo.lock b/Cargo.lock index 14c5115da..6406dbe6a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1679,6 +1679,7 @@ dependencies = [ "datadog-profiling", "ddcommon", "ddcommon-ffi", + "ddsketch-ffi", "ddtelemetry-ffi", "function_name", "futures", @@ -1964,6 +1965,15 @@ dependencies = [ "serde", ] +[[package]] +name = "ddsketch-ffi" +version = "19.0.1" +dependencies = [ + "build_common", + "datadog-ddsketch", + "ddcommon-ffi", +] + [[package]] name = "ddtelemetry" version = "19.0.1" diff --git a/Cargo.toml b/Cargo.toml index 2f103afa0..8a212f4bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,7 @@ members = [ "data-pipeline", "data-pipeline-ffi", "ddsketch", + "ddsketch-ffi", "tinybytes", "dogstatsd-client", "datadog-log", diff --git a/LICENSE-3rdparty.yml b/LICENSE-3rdparty.yml index 0c41e709b..e7ebd850e 100644 --- a/LICENSE-3rdparty.yml +++ b/LICENSE-3rdparty.yml @@ -1,4 +1,4 @@ -root_name: builder, build_common, tools, datadog-alloc, datadog-crashtracker, ddcommon, ddtelemetry, datadog-ddsketch, datadog-crashtracker-ffi, ddcommon-ffi, datadog-ipc, datadog-ipc-macros, tarpc, tarpc-plugins, tinybytes, spawn_worker, cc_utils, datadog-library-config, datadog-library-config-ffi, datadog-live-debugger, datadog-live-debugger-ffi, datadog-profiling, datadog-profiling-protobuf, datadog-profiling-ffi, data-pipeline-ffi, data-pipeline, datadog-trace-protobuf, datadog-trace-utils, datadog-trace-normalization, dogstatsd-client, datadog-log-ffi, datadog-log, ddtelemetry-ffi, symbolizer-ffi, datadog-profiling-replayer, datadog-remote-config, datadog-sidecar, datadog-sidecar-macros, datadog-sidecar-ffi, datadog-trace-obfuscation, datadog-tracer-flare, sidecar_mockgen, test_spawn_from_lib +root_name: builder, build_common, tools, datadog-alloc, datadog-crashtracker, ddcommon, ddtelemetry, datadog-ddsketch, datadog-crashtracker-ffi, ddcommon-ffi, datadog-ipc, datadog-ipc-macros, tarpc, tarpc-plugins, tinybytes, spawn_worker, cc_utils, datadog-library-config, datadog-library-config-ffi, datadog-live-debugger, datadog-live-debugger-ffi, datadog-profiling, datadog-profiling-protobuf, datadog-profiling-ffi, data-pipeline-ffi, data-pipeline, datadog-trace-protobuf, datadog-trace-utils, datadog-trace-normalization, dogstatsd-client, datadog-log-ffi, datadog-log, ddsketch-ffi, ddtelemetry-ffi, symbolizer-ffi, datadog-profiling-replayer, datadog-remote-config, datadog-sidecar, datadog-sidecar-macros, datadog-sidecar-ffi, datadog-trace-obfuscation, datadog-tracer-flare, sidecar_mockgen, test_spawn_from_lib third_party_libraries: - package_name: addr2line package_version: 0.24.2 diff --git a/build-profiling-ffi.sh b/build-profiling-ffi.sh index d9f30f16d..47af707e4 100755 --- a/build-profiling-ffi.sh +++ b/build-profiling-ffi.sh @@ -153,6 +153,7 @@ FEATURES=( "datadog-profiling-ffi/demangler" "datadog-library-config-ffi" "datadog-log-ffi" + "ddsketch-ffi" ) if [[ "$symbolizer" -eq 1 ]]; then FEATURES+=("symbolizer") @@ -237,7 +238,7 @@ echo "Generating $destdir/include/libdatadog headers..." rm -r $destdir/include/datadog/ mkdir $destdir/include/datadog/ -CBINDGEN_HEADERS="common.h profiling.h telemetry.h crashtracker.h data-pipeline.h library-config.h log.h" +CBINDGEN_HEADERS="common.h profiling.h telemetry.h crashtracker.h data-pipeline.h library-config.h log.h ddsketch.h" # When optional features are added, don't forget to also include the headers here case $ARG_FEATURES in esac diff --git a/builder/Cargo.toml b/builder/Cargo.toml index d0dc88d44..b9a469a3b 100644 --- a/builder/Cargo.toml +++ b/builder/Cargo.toml @@ -15,6 +15,7 @@ data-pipeline = [] symbolizer = [] library-config = [] log = [] +ddsketch = [] [lib] bench = false diff --git a/builder/src/bin/release.rs b/builder/src/bin/release.rs index 59fd560fd..2dcae756f 100644 --- a/builder/src/bin/release.rs +++ b/builder/src/bin/release.rs @@ -70,6 +70,8 @@ pub fn main() { f.push("datadog-library-config-ffi".to_string()); #[cfg(feature = "log")] f.push("datadog-log-ffi".to_string()); + #[cfg(feature = "ddsketch")] + f.push("ddsketch-ffi".to_string()); f }; diff --git a/builder/src/profiling.rs b/builder/src/profiling.rs index 4517b5685..9bdc98f3a 100644 --- a/builder/src/profiling.rs +++ b/builder/src/profiling.rs @@ -55,6 +55,8 @@ impl Profiling { headers.push("library-config.h"); #[cfg(feature = "log")] headers.push("log.h"); + #[cfg(feature = "ddsketch")] + headers.push("ddsketch.h"); let mut origin_path: PathBuf = [&self.source_include, "dummy.h"].iter().collect(); let mut target_path: PathBuf = [&self.target_include, "dummy.h"].iter().collect(); diff --git a/datadog-profiling-ffi/Cargo.toml b/datadog-profiling-ffi/Cargo.toml index 275959d42..eb915d252 100644 --- a/datadog-profiling-ffi/Cargo.toml +++ b/datadog-profiling-ffi/Cargo.toml @@ -29,6 +29,7 @@ crashtracker-receiver = ["crashtracker-ffi", "datadog-crashtracker-ffi/receiver" demangler = ["crashtracker-ffi", "datadog-crashtracker-ffi/demangler"] datadog-library-config-ffi = ["dep:datadog-library-config-ffi"] ddcommon-ffi = ["dep:ddcommon-ffi"] +ddsketch-ffi = ["dep:ddsketch-ffi"] [build-dependencies] build_common = { path = "../build-common" } @@ -43,6 +44,7 @@ ddcommon = { path = "../ddcommon" } ddcommon-ffi = { path = "../ddcommon-ffi", default-features = false, optional = true } ddtelemetry-ffi = { path = "../ddtelemetry-ffi", default-features = false, optional = true, features = ["expanded_builder_macros"] } datadog-log-ffi = { path = "../datadog-log-ffi", default-features = false, optional = true } +ddsketch-ffi = { path = "../ddsketch-ffi", default-features = false, optional = true } function_name = "0.3.0" futures = { version = "0.3", default-features = false } http-body-util = "0.1" diff --git a/datadog-profiling-ffi/src/lib.rs b/datadog-profiling-ffi/src/lib.rs index b070cce1a..bd8302c1a 100644 --- a/datadog-profiling-ffi/src/lib.rs +++ b/datadog-profiling-ffi/src/lib.rs @@ -26,6 +26,11 @@ pub use ddtelemetry_ffi::*; #[allow(unused_imports)] pub use data_pipeline_ffi::*; +// re-export ddsketch ffi +#[cfg(feature = "ddsketch-ffi")] +#[allow(unused_imports)] +pub use ddsketch_ffi::*; + // re-export library-config ffi #[cfg(feature = "datadog-library-config-ffi")] pub use datadog_library_config_ffi::*; diff --git a/ddcommon-ffi/src/cstr.rs b/ddcommon-ffi/src/cstr.rs index 4c1fd66dc..ea0e7a42a 100644 --- a/ddcommon-ffi/src/cstr.rs +++ b/ddcommon-ffi/src/cstr.rs @@ -60,6 +60,30 @@ impl CString { Ok(Self::from_std(std::ffi::CString::new(t)?)) } + /// Creates a new `CString` from the given input, or returns an empty `CString` + /// if the input contains null bytes. + /// + /// This method will never panic, as an empty string is guaranteed to not contain + /// null bytes and can always be converted to a `CString`. + /// + /// # Examples + /// + /// ``` + /// use ddcommon_ffi::CString; + /// + /// let good = CString::new_or_empty("hello world"); + /// assert_eq!(good.as_cstr().into_std().to_str().unwrap(), "hello world"); + /// + /// let bad = CString::new_or_empty("hello\0world"); + /// assert_eq!(bad.as_cstr().into_std().to_str().unwrap(), ""); + /// ``` + pub fn new_or_empty>>(t: T) -> Self { + Self::new(t).unwrap_or_else(|_| { + #[allow(clippy::unwrap_used)] + Self::new("").unwrap() + }) + } + pub fn as_cstr(&self) -> CStr<'_> { CStr { ptr: self.ptr, @@ -118,6 +142,18 @@ mod tests { assert_eq!(s.as_cstr().into_std().to_str().unwrap(), "hello"); } + #[test] + fn test_cstring_new_or_empty() { + let good = CString::new_or_empty("hello world"); + assert_eq!(good.as_cstr().into_std().to_str().unwrap(), "hello world"); + + let bad = CString::new_or_empty("hello\0world"); + assert_eq!(bad.as_cstr().into_std().to_str().unwrap(), ""); + + let empty = CString::new_or_empty(""); + assert_eq!(empty.as_cstr().into_std().to_str().unwrap(), ""); + } + #[test] fn test_raw_cstr() { let s: &'static [u8] = b"abc\0"; diff --git a/ddsketch-ffi/Cargo.toml b/ddsketch-ffi/Cargo.toml new file mode 100644 index 000000000..f5e839158 --- /dev/null +++ b/ddsketch-ffi/Cargo.toml @@ -0,0 +1,26 @@ +# Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "ddsketch-ffi" +edition.workspace = true +version.workspace = true +rust-version.workspace = true +license.workspace = true + +[lib] +crate-type = ["lib", "staticlib", "cdylib"] +bench = false + +[features] +default = ["cbindgen"] +cbindgen = ["build_common/cbindgen", "ddcommon-ffi/cbindgen"] + +[build-dependencies] +build_common = { path = "../build-common" } + +[dependencies] +datadog-ddsketch = { path = "../ddsketch" } +ddcommon-ffi = { path = "../ddcommon-ffi", default-features = false } + +[dev-dependencies] diff --git a/ddsketch-ffi/build.rs b/ddsketch-ffi/build.rs new file mode 100644 index 000000000..c288152f6 --- /dev/null +++ b/ddsketch-ffi/build.rs @@ -0,0 +1,11 @@ +// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +extern crate build_common; + +use build_common::generate_and_configure_header; + +fn main() { + let header_name = "ddsketch.h"; + generate_and_configure_header(header_name); +} diff --git a/ddsketch-ffi/cbindgen.toml b/ddsketch-ffi/cbindgen.toml new file mode 100644 index 000000000..4fa7470cc --- /dev/null +++ b/ddsketch-ffi/cbindgen.toml @@ -0,0 +1,35 @@ +# Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ +# SPDX-License-Identifier: Apache-2.0 + +language = "C" +cpp_compat = true +tab_width = 2 +header = """// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 +""" +include_guard = "DDOG_DDSKETCH_H" +style = "both" +pragma_once = true +no_includes = true +sys_includes = ["stdbool.h", "stddef.h", "stdint.h"] +includes = ["common.h"] + +[parse] +parse_deps = true +include = ["ddcommon-ffi", "datadog-ddsketch"] + +[export] +include = ["ddsketch-ffi"] +prefix = "ddog_" +renaming_overrides_prefixing = true + +[export.rename] +"DDSketchError" = "DDSketchError" +"DDSketchErrorCode" = "DDSketchErrorCode" + +[fn] +must_use = "DDOG_CHECK_RETURN" + +[enum] +prefix_with_name = true +rename_variants = "ScreamingSnakeCase" diff --git a/ddsketch-ffi/src/error.rs b/ddsketch-ffi/src/error.rs new file mode 100644 index 000000000..8f596f507 --- /dev/null +++ b/ddsketch-ffi/src/error.rs @@ -0,0 +1,97 @@ +// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +use ddcommon_ffi::CString; +use std::fmt::Display; + +/// Represent error codes that `DDSketchError` struct can hold +#[repr(C)] +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum DDSketchErrorCode { + InvalidArgument, + InvalidInput, + Internal, +} + +impl Display for DDSketchErrorCode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::InvalidArgument => write!(f, "Invalid argument provided"), + Self::InvalidInput => write!(f, "Invalid input"), + Self::Internal => write!(f, "Internal error"), + } + } +} + +/// Structure that contains error information that DDSketch FFI API can return. +#[repr(C)] +#[derive(Debug)] +pub struct DDSketchError { + pub code: DDSketchErrorCode, + pub msg: CString, +} + +impl DDSketchError { + pub fn new(code: DDSketchErrorCode, msg: &str) -> Self { + Self { + code, + msg: CString::new_or_empty(msg), + } + } +} + +impl From> for DDSketchError { + fn from(value: Box) -> Self { + DDSketchError::new(DDSketchErrorCode::Internal, &value.to_string()) + } +} + +/// Frees `error` and all its contents. After being called error will not point to a valid memory +/// address so any further actions on it could lead to undefined behavior. +/// +/// # Safety +/// +/// Only pass null or a pointer to a valid DDSketchError created by this library. +#[no_mangle] +pub unsafe extern "C" fn ddog_ddsketch_error_free(error: Option>) { + drop(error) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn constructor_test() { + let code = DDSketchErrorCode::InvalidArgument; + let error = Box::new(DDSketchError::new(code, &code.to_string())); + + assert_eq!(error.code, DDSketchErrorCode::InvalidArgument); + let msg = error.msg.as_cstr().into_std().to_str().unwrap(); + assert_eq!(msg, DDSketchErrorCode::InvalidArgument.to_string()); + } + + #[test] + fn destructor_test() { + let code = DDSketchErrorCode::InvalidArgument; + let error = Box::new(DDSketchError::new(code, &code.to_string())); + + assert_eq!(error.code, DDSketchErrorCode::InvalidArgument); + let msg = error.msg.as_cstr().into_std().to_str().unwrap(); + assert_eq!(msg, DDSketchErrorCode::InvalidArgument.to_string()); + + unsafe { ddog_ddsketch_error_free(Some(error)) }; + } + + #[test] + fn test_error_with_null_bytes() { + let code = DDSketchErrorCode::InvalidInput; + let error = Box::new(DDSketchError::new(code, "Error with\0null bytes")); + + assert_eq!(error.code, DDSketchErrorCode::InvalidInput); + let msg = error.msg.as_cstr().into_std().to_str().unwrap(); + assert_eq!(msg, ""); // Should fall back to empty string + + unsafe { ddog_ddsketch_error_free(Some(error)) }; + } +} diff --git a/ddsketch-ffi/src/lib.rs b/ddsketch-ffi/src/lib.rs new file mode 100644 index 000000000..d49bd23c0 --- /dev/null +++ b/ddsketch-ffi/src/lib.rs @@ -0,0 +1,196 @@ +// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +#![cfg_attr(not(test), deny(clippy::panic))] +#![cfg_attr(not(test), deny(clippy::unwrap_used))] +#![cfg_attr(not(test), deny(clippy::expect_used))] +#![cfg_attr(not(test), deny(clippy::todo))] +#![cfg_attr(not(test), deny(clippy::unimplemented))] + +use datadog_ddsketch::DDSketch; +use ddcommon_ffi as ffi; +use std::ptr::NonNull; + +mod error; +mod sketch; + +pub use error::*; +pub use sketch::*; + +macro_rules! gen_error { + ($l:expr) => { + Some(Box::new(DDSketchError::new($l, &$l.to_string()))) + }; +} + +/// Creates a new DDSketch instance with default configuration. +/// +/// # Safety +/// +/// The `sketch` parameter must be a valid pointer to uninitialized memory +/// where the DDSketch will be stored. +#[no_mangle] +pub unsafe extern "C" fn ddog_ddsketch_new( + sketch: NonNull>, +) -> Option> { + let sketch_box = Box::new(DDSketch::default()); + sketch.as_ptr().write(sketch_box); + None +} + +/// Drops a DDSketch instance. +/// +/// # Safety +/// +/// Only pass null or a pointer to a valid, mutable DDSketch. +#[no_mangle] +pub unsafe extern "C" fn ddog_ddsketch_drop(sketch: Option>) { + drop(sketch); +} + +/// Adds a point to the DDSketch. +/// +/// # Safety +/// +/// The `sketch` parameter must be a valid pointer to a DDSketch instance. +#[no_mangle] +pub unsafe extern "C" fn ddog_ddsketch_add( + sketch: Option<&mut DDSketch>, + point: f64, +) -> Option> { + let sketch = match sketch { + Some(s) => s, + None => return gen_error!(DDSketchErrorCode::InvalidArgument), + }; + + match sketch.add(point) { + Ok(_) => None, + Err(e) => Some(Box::new(DDSketchError::new( + DDSketchErrorCode::InvalidInput, + &e.to_string(), + ))), + } +} + +/// Adds a point with a specific count to the DDSketch. +/// +/// # Safety +/// +/// The `sketch` parameter must be a valid pointer to a DDSketch instance. +#[no_mangle] +pub unsafe extern "C" fn ddog_ddsketch_add_with_count( + sketch: Option<&mut DDSketch>, + point: f64, + count: f64, +) -> Option> { + let sketch = match sketch { + Some(s) => s, + None => return gen_error!(DDSketchErrorCode::InvalidArgument), + }; + + match sketch.add_with_count(point, count) { + Ok(_) => None, + Err(e) => Some(Box::new(DDSketchError::new( + DDSketchErrorCode::InvalidInput, + &e.to_string(), + ))), + } +} + +/// Returns the count of points in the DDSketch. +/// +/// # Safety +/// +/// The `sketch` parameter must be a valid pointer to a DDSketch instance. +/// Returns 0.0 if sketch is null. +#[no_mangle] +pub unsafe extern "C" fn ddog_ddsketch_count(sketch: Option<&DDSketch>) -> f64 { + match sketch { + Some(s) => s.count(), + None => 0.0, + } +} + +/// Returns the protobuf-encoded bytes of the DDSketch. +/// +/// # Safety +/// +/// The `sketch` parameter must be a valid pointer to a DDSketch instance. +/// The returned vector must be freed with `ddog_Vec_U8_drop`. +#[no_mangle] +pub unsafe extern "C" fn ddog_ddsketch_encode(sketch: Box) -> ffi::Vec { + let encoded = sketch.encode_to_vec(); + ffi::Vec::from(encoded) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::mem::MaybeUninit; + use std::ptr::NonNull; + + #[test] + fn test_ddsketch_new_and_drop() { + unsafe { + let mut sketch: MaybeUninit> = MaybeUninit::uninit(); + let result = ddog_ddsketch_new(NonNull::new(sketch.as_mut_ptr()).unwrap()); + assert!(result.is_none()); + + let sketch_box = sketch.assume_init(); + ddog_ddsketch_drop(Some(sketch_box)); + } + } + + #[test] + fn test_ddsketch_add() { + unsafe { + let mut sketch: MaybeUninit> = MaybeUninit::uninit(); + ddog_ddsketch_new(NonNull::new(sketch.as_mut_ptr()).unwrap()); + let mut sketch_box = sketch.assume_init(); + + let result = ddog_ddsketch_add(Some(&mut sketch_box), 1.0); + assert!(result.is_none()); + + let count = ddog_ddsketch_count(Some(&sketch_box)); + assert_eq!(count, 1.0); + + ddog_ddsketch_drop(Some(sketch_box)); + } + } + + #[test] + fn test_ddsketch_add_with_count() { + unsafe { + let mut sketch: MaybeUninit> = MaybeUninit::uninit(); + ddog_ddsketch_new(NonNull::new(sketch.as_mut_ptr()).unwrap()); + let mut sketch_box = sketch.assume_init(); + + let result = ddog_ddsketch_add_with_count(Some(&mut sketch_box), 2.0, 3.0); + assert!(result.is_none()); + + let count = ddog_ddsketch_count(Some(&sketch_box)); + assert_eq!(count, 3.0); + + ddog_ddsketch_drop(Some(sketch_box)); + } + } + + #[test] + fn test_ddsketch_encode() { + unsafe { + let mut sketch: MaybeUninit> = MaybeUninit::uninit(); + ddog_ddsketch_new(NonNull::new(sketch.as_mut_ptr()).unwrap()); + let mut sketch_box = sketch.assume_init(); + + let _ = ddog_ddsketch_add(Some(&mut sketch_box), 1.0); + let _ = ddog_ddsketch_add(Some(&mut sketch_box), 2.0); + + let encoded = ddog_ddsketch_encode(sketch_box); + assert!(!encoded.is_empty()); + + // Note: In a real implementation, the caller would need to drop the Vec + // For now, we'll just let it be consumed by the test + drop(encoded); + } + } +} diff --git a/ddsketch-ffi/src/sketch.rs b/ddsketch-ffi/src/sketch.rs new file mode 100644 index 000000000..d582d51c4 --- /dev/null +++ b/ddsketch-ffi/src/sketch.rs @@ -0,0 +1,98 @@ +// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +use datadog_ddsketch::DDSketch; +use ddcommon_ffi as ffi; + +/// A bin from a DDSketch containing a value and its weight. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct DDSketchBin { + pub value: f64, + pub weight: f64, +} + +/// Returns the ordered bins from the DDSketch. +/// +/// # Safety +/// +/// The `sketch` parameter must be a valid pointer to a DDSketch instance. +/// The returned bins must be freed with `ddog_ddsketch_bins_drop`. +/// Returns empty bins if sketch is null. +#[no_mangle] +pub unsafe extern "C" fn ddog_ddsketch_ordered_bins( + sketch: Option<&DDSketch>, +) -> ffi::Vec { + let sketch = match sketch { + Some(s) => s, + None => return ffi::Vec::new(), + }; + + let bins = sketch.ordered_bins(); + let result: Vec = bins + .into_iter() + .map(|(value, weight)| DDSketchBin { value, weight }) + .collect(); + + ffi::Vec::from(result) +} + +/// Drops a DDSketchBins instance. +/// +/// # Safety +/// +/// Only pass a valid DDSketchBins instance. +#[no_mangle] +pub unsafe extern "C" fn ddog_ddsketch_bins_drop(bins: ffi::Vec) { + drop(bins); +} + +#[cfg(test)] +mod tests { + use super::*; + use datadog_ddsketch::DDSketch; + + #[test] + fn test_ddsketch_bins() { + let mut sketch = DDSketch::default(); + sketch.add(1.0).unwrap(); + sketch.add(2.0).unwrap(); + sketch.add(3.0).unwrap(); + + unsafe { + let bins = ddog_ddsketch_ordered_bins(Some(&sketch)); + assert!(!bins.is_empty()); + + ddog_ddsketch_bins_drop(bins); + } + } + + #[test] + fn test_ddsketch_bins_manual() { + let bins_vec = vec![ + DDSketchBin { + value: 1.0, + weight: 1.0, + }, + DDSketchBin { + value: 2.0, + weight: 1.0, + }, + ]; + + let bins = ffi::Vec::from(bins_vec); + assert_eq!(bins.len(), 2); + assert!(!bins.is_empty()); + + // Test that we can access the data through the slice + let slice = bins.as_slice(); + assert_eq!(slice[0].value, 1.0); + assert_eq!(slice[0].weight, 1.0); + assert_eq!(slice[1].value, 2.0); + assert_eq!(slice[1].weight, 1.0); + + unsafe { + ddog_ddsketch_bins_drop(bins); + } + } +} diff --git a/examples/ffi/CMakeLists.txt b/examples/ffi/CMakeLists.txt index b0049ca66..457a019ec 100644 --- a/examples/ffi/CMakeLists.txt +++ b/examples/ffi/CMakeLists.txt @@ -74,3 +74,7 @@ set_vcruntime_link_type(array_queue ${VCRUNTIME_LINK_TYPE}) add_executable(library_config library_config.c) target_link_libraries(library_config PRIVATE Datadog::Profiling) set_vcruntime_link_type(library_config ${VCRUNTIME_LINK_TYPE}) + +add_executable(ddsketch ddsketch.c) +target_link_libraries(ddsketch PRIVATE Datadog::Profiling) +set_vcruntime_link_type(ddsketch ${VCRUNTIME_LINK_TYPE}) diff --git a/examples/ffi/README.md b/examples/ffi/README.md index 0acc71ff9..54c856889 100644 --- a/examples/ffi/README.md +++ b/examples/ffi/README.md @@ -3,7 +3,7 @@ In order to be able to run FFI examples, you need to build the shared library and headers with the command: ```bash -cargo run --bin release --features profiling,telemetry,data-pipeline,symbolizer,crashtracker,library-config,log --release -- --out +cargo run --bin release --features profiling,telemetry,data-pipeline,symbolizer,crashtracker,library-config,log,ddsketch --release -- --out ``` You can then build the examples with: diff --git a/examples/ffi/ddsketch.c b/examples/ffi/ddsketch.c new file mode 100644 index 000000000..4ada45702 --- /dev/null +++ b/examples/ffi/ddsketch.c @@ -0,0 +1,74 @@ +// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include +#include + +#define TRY(expr) \ + { \ + struct DDSketchError *err = expr; \ + if (err != NULL) { \ + const char *message = err->msg.ptr; \ + fprintf(stderr, "ERROR: %s\n", message); \ + ddog_ddsketch_error_free(err); \ + return 1; \ + } \ + } + +int main(void) { + // Create a new DDSketch + ddog_DDSketch *sketch = NULL; + TRY(ddog_ddsketch_new(&sketch)); + + printf("Created DDSketch successfully\n"); + + // Add some sample data points + printf("Adding sample data points...\n"); + TRY(ddog_ddsketch_add(sketch, 1.0)); + TRY(ddog_ddsketch_add(sketch, 2.5)); + TRY(ddog_ddsketch_add(sketch, 5.0)); + TRY(ddog_ddsketch_add(sketch, 10.0)); + TRY(ddog_ddsketch_add(sketch, 15.0)); + + // Add points with specific counts + printf("Adding points with specific counts...\n"); + TRY(ddog_ddsketch_add_with_count(sketch, 3.0, 5.0)); // Add 3.0 with count 5 + TRY(ddog_ddsketch_add_with_count(sketch, 7.0, 3.0)); // Add 7.0 with count 3 + + // Get the total count + double count = ddog_ddsketch_count(sketch); + printf("Total count in sketch: %.0f\n", count); + + // Get the ordered bins (buckets) + printf("Getting ordered bins...\n"); + ddog_Vec_DDSketchBin bins = ddog_ddsketch_ordered_bins(sketch); + + printf("Number of bins: %zu\n", bins.len); + for (size_t i = 0; i < bins.len; i++) { + printf(" Bin %zu: value=%.2f, weight=%.0f\n", i, bins.ptr[i].value, bins.ptr[i].weight); + } + + // Clean up bins + ddog_ddsketch_bins_drop(bins); + + // Encode the sketch to protobuf format + printf("Encoding sketch to protobuf...\n"); + struct ddog_Vec_u8 encoded = ddog_ddsketch_encode(sketch); + + printf("Encoded sketch size: %zu bytes\n", encoded.len); + + // Print first few bytes of encoded data (for demonstration) + printf("First 10 bytes of encoded data: "); + for (size_t i = 0; i < (encoded.len < 10 ? encoded.len : 10); i++) { + printf("%02x ", encoded.ptr[i]); + } + printf("\n"); + + // Clean up the sketch (note: sketch is consumed by ddog_ddsketch_encode) + // ddog_ddsketch_drop is not called here because the sketch was consumed + + printf("DDSketch example completed successfully!\n"); + return 0; +} diff --git a/tools/docker/Dockerfile.build b/tools/docker/Dockerfile.build index 80527260b..8bca8da7c 100644 --- a/tools/docker/Dockerfile.build +++ b/tools/docker/Dockerfile.build @@ -79,6 +79,7 @@ COPY "ddtelemetry-ffi/Cargo.toml" "ddtelemetry-ffi/" COPY "datadog-log/Cargo.toml" "datadog-log/" COPY "datadog-log-ffi/Cargo.toml" "datadog-log-ffi/" COPY "ddsketch/Cargo.toml" "ddsketch/" +COPY "ddsketch-ffi/Cargo.toml" "ddsketch-ffi/" COPY "dogstatsd-client/Cargo.toml" "dogstatsd-client/" COPY "datadog-library-config-ffi/Cargo.toml" "datadog-library-config-ffi/" COPY "datadog-library-config/Cargo.toml" "datadog-library-config/"