Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions examples/ffi/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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})
35 changes: 35 additions & 0 deletions examples/ffi/ffe_dependencies.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/
// SPDX-License-Identifier: Apache-2.0
#include <stdio.h>
#include <string.h>
#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;
}
229 changes: 206 additions & 23 deletions libdd-ffe-ffi/src/assignment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pub struct ResolutionDetails {
// memoizing some fields, so we can hand off references to them:
error_message: Option<String>,
flag_metadata: Vec<KeyValue<BorrowedStr, BorrowedStr>>,
dependencies: Vec<ffe::EvaluationRecord>,
}
impl ResolutionDetails {
fn new(value: Result<Assignment, EvaluationError>) -> ResolutionDetails {
Expand All @@ -44,6 +45,7 @@ impl ResolutionDetails {
inner,
error_message,
flag_metadata,
dependencies: Vec::new(),
}
}
}
Expand Down Expand Up @@ -156,33 +158,123 @@ pub unsafe extern "C" fn ddog_ffe_get_assignment(
expected_type: ExpectedFlagType,
context: Handle<EvaluationContext>,
) -> Handle<ResolutionDetails> {
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<ResolutionDetails>,
) -> Handle<ResolutionDetails> {
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<ResolutionDetails>,
) -> 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<ResolutionDetails>,
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<ResolutionDetails>,
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<ResolutionDetails>,
index: usize,
) -> Handle<ResolutionDetails> {
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)]
Expand Down Expand Up @@ -490,6 +582,97 @@ pub unsafe extern "C" fn ddog_ffe_assignment_drop(assignment: *mut Handle<Resolu
mod tests {
use super::*;

#[test]
fn prerequisite_handles_preserve_errors_eligibility_and_ownership() {
let json = br#"{
"createdAt":"2026-01-01T00:00:00Z","environment":{"name":"test"},
"flags":{
"parent":{"key":"parent","enabled":true,"variationType":"BOOLEAN",
"variations":{"on":{"key":"on","value":true}},
"allocations":[{"key":"parent-allocation","doLog":true,"splits":[{"variationKey":"on","shards":[]}]}]},
"root":{"key":"root","enabled":true,"variationType":"BOOLEAN",
"variations":{"on":{"key":"on","value":true}},
"allocations":[{"key":"root-allocation","rules":[{"conditions":[
{"flagEvaluation":{"key":"parent"},"operator":"ONE_OF","value":["on"]},
{"flagEvaluation":{"key":"parent"},"operator":"ONE_OF","value":["on"]}
]}],"splits":[{"variationKey":"on","shards":[]}]}]}
}}"#;
for prerequisite in ["parent", "missing"] {
let wire = String::from_utf8(json.to_vec()).unwrap().replace(
"\"flagEvaluation\":{\"key\":\"parent\"}",
&format!("\"flagEvaluation\":{{\"key\":\"{prerequisite}\"}}"),
);
let config = Configuration::from_server_response(
ffe::UniversalFlagConfig::from_json(wire.into_bytes()).unwrap(),
);
let context = EvaluationContext::new(Some("subject".into()), Default::default());
let trace = config.eval_flag_with_trace(
"root",
&context,
ffe::ExpectedFlagType::Boolean,
now(),
Default::default(),
);
let mut details = ResolutionDetails::new(trace.result);
details.dependencies = trace.evaluations;
let mut root = Handle::new(details);
// SAFETY: the handle copies are borrowed only until root is freed below.
let count_handle = unsafe { std::ptr::read(&root) };
assert_eq!(
// SAFETY: count_handle borrows the live root.
unsafe { ddog_ffe_assignment_dependency_count(count_handle) },
1
);
// SAFETY: this copy borrows the live root.
let key_handle = unsafe { std::ptr::read(&root) };
// SAFETY: key_handle remains valid throughout this call.
let key = unsafe { ddog_ffe_assignment_dependency_key(key_handle, 0) };
assert_eq!(
// SAFETY: the key is borrowed from the live root.
unsafe { std::slice::from_raw_parts(key.ptr, key.len) },
prerequisite.as_bytes()
);
// SAFETY: this copy borrows the live root.
let eligibility_handle = unsafe { std::ptr::read(&root) };
assert_eq!(
// SAFETY: eligibility_handle remains valid throughout this call.
unsafe { ddog_ffe_assignment_dependency_exposure_eligible(eligibility_handle, 0) },
prerequisite == "parent"
);
// SAFETY: this copy borrows the live root.
let result_handle = unsafe { std::ptr::read(&root) };
// SAFETY: result_handle is live; the returned copy owns its result independently.
let mut dependency = unsafe { ddog_ffe_assignment_dependency_result(result_handle, 0) };
// SAFETY: all root borrows ended.
unsafe { ddog_ffe_assignment_drop(&mut root) };
// SAFETY: the child owns its result and survives the root's destruction.
let error_handle = unsafe { std::ptr::read(&dependency) };
assert_eq!(
// SAFETY: error_handle borrows the live child.
unsafe { ddog_ffe_assignment_get_error_code(error_handle) },
if prerequisite == "parent" {
ErrorCode::Ok
} else {
ErrorCode::FlagNotFound
}
);
if prerequisite == "parent" {
// SAFETY: this copy borrows the live child.
let allocation_handle = unsafe { std::ptr::read(&dependency) };
// SAFETY: allocation_handle remains valid throughout the call.
let allocation =
unsafe { ddog_ffe_assignment_get_allocation_key(allocation_handle) };
assert_eq!(
// SAFETY: allocation borrows the still-live child.
unsafe { std::slice::from_raw_parts(allocation.ptr, allocation.len) },
b"parent-allocation"
);
}
// SAFETY: all child borrows ended; free its owned copy once.
unsafe { ddog_ffe_assignment_drop(&mut dependency) };
}
}

#[test]
fn rejected_flag_is_exposed_as_parse_error() {
let details = ResolutionDetails::from(EvaluationError::FlagConfigurationInvalid);
Expand Down
Loading