From 4769b0530384c7f9f5ece21878bc98416ac9f29a Mon Sep 17 00:00:00 2001 From: Helen Han Date: Mon, 2 Nov 2026 20:33:46 -0500 Subject: [PATCH] feat(ffe): backport prerequisite FFI accessors to v46 Expose prerequisite observations on the native ABI used by the Ruby SDK. Environment: Datadog workspace Co-Authored-By: Codex GPT-6 --- examples/ffi/CMakeLists.txt | 4 + examples/ffi/ffe_dependencies.c | 35 +++++ libdd-ffe-ffi/src/assignment.rs | 229 ++++++++++++++++++++++++++++---- 3 files changed, 245 insertions(+), 23 deletions(-) create mode 100644 examples/ffi/ffe_dependencies.c diff --git a/examples/ffi/CMakeLists.txt b/examples/ffi/CMakeLists.txt index 60a061278f..04c7433b2d 100644 --- a/examples/ffi/CMakeLists.txt +++ b/examples/ffi/CMakeLists.txt @@ -110,3 +110,7 @@ set_vcruntime_link_type(ffe ${VCRUNTIME_LINK_TYPE}) if(MSVC) target_compile_definitions(ffe PRIVATE _CRT_SECURE_NO_WARNINGS) endif() + +add_executable(ffe_dependencies ffe_dependencies.c) +target_link_libraries(ffe_dependencies PRIVATE Datadog::Profiling) +set_vcruntime_link_type(ffe_dependencies ${VCRUNTIME_LINK_TYPE}) diff --git a/examples/ffi/ffe_dependencies.c b/examples/ffi/ffe_dependencies.c new file mode 100644 index 0000000000..956ceea60d --- /dev/null +++ b/examples/ffi/ffe_dependencies.c @@ -0,0 +1,35 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include "datadog/ffe.h" + +#define CHECK(condition) do { if (!(condition)) { fprintf(stderr, "failed: %s\n", #condition); return 1; } } while (0) + +int main(void) { + const char *json = "{\"createdAt\":\"2026-01-01T00:00:00Z\",\"environment\":{\"name\":\"test\"},\"observeFullEvaluationData\":false,\"flags\":{\"root\":{\"key\":\"root\",\"enabled\":true,\"variationType\":\"BOOLEAN\",\"variations\":{\"on\":{\"key\":\"on\",\"value\":true},\"off\":{\"key\":\"off\",\"value\":false}},\"allocations\":[{\"key\":\"root-allocation\",\"doLog\":true,\"splits\":[{\"variationKey\":\"on\",\"shards\":[]}],\"rules\":[{\"conditions\":[{\"flagEvaluation\":{\"key\":\"parent\"},\"operator\":\"ONE_OF\",\"value\":[\"on\"]},{\"flagEvaluation\":{\"key\":\"parent\"},\"operator\":\"ONE_OF\",\"value\":[\"on\"]}]}]}]},\"parent\":{\"key\":\"parent\",\"enabled\":true,\"variationType\":\"BOOLEAN\",\"variations\":{\"on\":{\"key\":\"on\",\"value\":true},\"off\":{\"key\":\"off\",\"value\":false}},\"allocations\":[{\"key\":\"parent-allocation\",\"doLog\":true,\"splits\":[{\"variationKey\":\"on\",\"shards\":[]}]}]},\"missing\":{\"key\":\"missing\",\"enabled\":true,\"variationType\":\"BOOLEAN\",\"variations\":{\"on\":{\"key\":\"on\",\"value\":true},\"off\":{\"key\":\"off\",\"value\":false}},\"allocations\":[{\"key\":\"missing-allocation\",\"doLog\":true,\"splits\":[{\"variationKey\":\"on\",\"shards\":[]}],\"rules\":[{\"conditions\":[{\"flagEvaluation\":{\"key\":\"absent\"},\"operator\":\"ONE_OF\",\"value\":[\"on\"]}]}]}]}}}"; + struct ddog_ffe_Result_HandleConfiguration parsed = ddog_ffe_configuration_new( + (struct ddog_ffe_BorrowedStr){.ptr = (const uint8_t *)json, .len = strlen(json)}); + CHECK(parsed.tag == DDOG_FFE_RESULT_HANDLE_CONFIGURATION_OK_HANDLE_CONFIGURATION); + ddog_ffe_Handle_Configuration config = parsed.ok; + ddog_ffe_Handle_EvaluationContext context = ddog_ffe_evaluation_context_new("subject", NULL, 0); + ddog_ffe_Handle_ResolutionDetails root = ddog_ffe_get_assignment( + config, "root", DDOG_FFE_EXPECTED_FLAG_TYPE_BOOLEAN, context); + CHECK(ddog_ffe_assignment_dependency_count(root) == 1); + struct ddog_ffe_BorrowedStr key = ddog_ffe_assignment_dependency_key(root, 0); + CHECK(key.len == 6 && memcmp(key.ptr, "parent", 6) == 0); + CHECK(ddog_ffe_assignment_dependency_exposure_eligible(root, 0)); + CHECK(ddog_ffe_assignment_dependency_key(root, 1).len == 0); + CHECK(!ddog_ffe_assignment_dependency_exposure_eligible(root, 1)); + ddog_ffe_Handle_ResolutionDetails child = ddog_ffe_assignment_dependency_result(root, 0); + ddog_ffe_assignment_drop(&root); + // Each child owns its data independently of the root result and configuration. + ddog_ffe_configuration_drop(&config); + CHECK(ddog_ffe_assignment_get_error_code(child) == DDOG_FFE_ERROR_CODE_OK); + struct ddog_ffe_VariantValue value = ddog_ffe_assignment_get_value(child); + CHECK(value.tag == DDOG_FFE_VARIANT_VALUE_BOOLEAN && value.boolean); + CHECK(ddog_ffe_assignment_dependency_count(child) == 0); + ddog_ffe_assignment_drop(&child); + ddog_ffe_evaluation_context_drop(&context); + return 0; +} diff --git a/libdd-ffe-ffi/src/assignment.rs b/libdd-ffe-ffi/src/assignment.rs index 7e1d7e0f8d..e79c4ad705 100644 --- a/libdd-ffe-ffi/src/assignment.rs +++ b/libdd-ffe-ffi/src/assignment.rs @@ -20,6 +20,7 @@ pub struct ResolutionDetails { // memoizing some fields, so we can hand off references to them: error_message: Option, flag_metadata: Vec>, + dependencies: Vec, } impl ResolutionDetails { fn new(value: Result) -> ResolutionDetails { @@ -44,6 +45,7 @@ impl ResolutionDetails { inner, error_message, flag_metadata, + dependencies: Vec::new(), } } } @@ -156,33 +158,123 @@ pub unsafe extern "C" fn ddog_ffe_get_assignment( expected_type: ExpectedFlagType, context: Handle, ) -> Handle { - if flag_key.is_null() { - return Handle::new( - EvaluationError::Internal(Str::from_static_str( - "ddog_ffe_get_assignment: flag_key must not be NULL", - )) - .into(), - ); - } + catch_assignment_panic(|| { + if flag_key.is_null() { + return Handle::new( + EvaluationError::Internal(Str::from_static_str( + "ddog_ffe_get_assignment: flag_key must not be NULL", + )) + .into(), + ); + } - // SAFETY: the caller must ensure that configuration handle is valid - let config = unsafe { config.as_ref() }; - // SAFETY: the caller must ensure that context handle is valid - let context = unsafe { context.as_ref() }; - - // SAFETY: we checked that flag_key is not NULL. - let Ok(flag_key) = unsafe { CStr::from_ptr(flag_key) }.to_str() else { - return Handle::new( - EvaluationError::Internal(Str::from_static_str( - "ddog_ffe_get_assignment: flag_key is not a valid UTF-8 string", - )) - .into(), + // SAFETY: the caller must ensure that configuration handle is valid + let config = unsafe { config.as_ref() }; + // SAFETY: the caller must ensure that context handle is valid + let context = unsafe { context.as_ref() }; + + // SAFETY: we checked that flag_key is not NULL. + let Ok(flag_key) = unsafe { CStr::from_ptr(flag_key) }.to_str() else { + return Handle::new( + EvaluationError::Internal(Str::from_static_str( + "ddog_ffe_get_assignment: flag_key is not a valid UTF-8 string", + )) + .into(), + ); + }; + + let evaluation = config.eval_flag_with_trace( + flag_key, + context, + expected_type.into(), + now(), + ffe::EvaluationOptions::default(), ); - }; + let mut details = ResolutionDetails::new(evaluation.result); + details.dependencies = evaluation.evaluations; + Handle::new(details) + }) +} - let assignment_result = config.eval_flag(flag_key, context, expected_type.into(), now()); +fn catch_assignment_panic( + evaluate: impl FnOnce() -> Handle, +) -> Handle { + std::panic::catch_unwind(std::panic::AssertUnwindSafe(evaluate)).unwrap_or_else(|_| { + Handle::new( + EvaluationError::Internal(Str::from_static_str("feature flag evaluation failed")) + .into(), + ) + }) +} + +/// Number of unique prerequisites reached by this evaluation. +/// # Safety +/// `assignment` must be a live handle. +#[unsafe(no_mangle)] +// allow(ffi-panic-boundary): reads the length of an owned vector. +pub unsafe extern "C" fn ddog_ffe_assignment_dependency_count( + assignment: Handle, +) -> usize { + // SAFETY: caller guarantees a live handle. + unsafe { assignment.as_ref() }.dependencies.len() +} - Handle::new(assignment_result.into()) +/// Prerequisite key, borrowed until the root assignment is freed; empty for an invalid index. +/// # Safety +/// `assignment` must be a live handle. +#[unsafe(no_mangle)] +// allow(ffi-panic-boundary): bounds-checked lookup and borrowing do not allocate. +pub unsafe extern "C" fn ddog_ffe_assignment_dependency_key( + assignment: Handle, + index: usize, +) -> BorrowedStr { + // SAFETY: caller guarantees a live handle. + match unsafe { assignment.as_ref() }.dependencies.get(index) { + // SAFETY: caller retains the root assignment throughout the borrow. + Some(record) => unsafe { BorrowedStr::borrow_from_str(&record.flag_key) }, + None => BorrowedStr::empty(), + } +} + +/// Whether this prerequisite contributed to the root's successful assignment. +/// This does not override the prerequisite's `do_log` or SDK consent and deduplication. +/// # Safety +/// `assignment` must be a live handle. +#[unsafe(no_mangle)] +// allow(ffi-panic-boundary): bounds-checked lookup of a boolean does not allocate. +pub unsafe extern "C" fn ddog_ffe_assignment_dependency_exposure_eligible( + assignment: Handle, + index: usize, +) -> bool { + // SAFETY: caller guarantees a live handle. + unsafe { assignment.as_ref() } + .dependencies + .get(index) + .is_some_and(|record| record.exposure_eligible) +} + +/// Copies a prerequisite result into an independently owned handle, including errors. +/// Free it with `ddog_ffe_assignment_drop`. Invalid indices return a General error. +/// # Safety +/// `assignment` must be a live handle. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn ddog_ffe_assignment_dependency_result( + assignment: Handle, + index: usize, +) -> Handle { + catch_assignment_panic(|| { + // SAFETY: caller guarantees a live handle. + let result = unsafe { assignment.as_ref() } + .dependencies + .get(index) + .map(|record| record.result.clone()) + .unwrap_or_else(|| { + Err(EvaluationError::Internal(Str::from_static_str( + "invalid prerequisite index", + ))) + }); + Handle::new(result.into()) + }) } #[repr(C)] @@ -490,6 +582,97 @@ pub unsafe extern "C" fn ddog_ffe_assignment_drop(assignment: *mut Handle