Skip to content
Draft
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
241 changes: 199 additions & 42 deletions Cargo.Bazel.lock

Large diffs are not rendered by default.

114 changes: 73 additions & 41 deletions Cargo.lock

Large diffs are not rendered by default.

47 changes: 24 additions & 23 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,29 +18,30 @@ android_logger = { version = "0.15.1", default-features = false }
anyhow = "1.0.99"
assert_matches = "1.5.0"
async-trait = "0.1.89"
bd-api = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "693e1e433eb7e531a7817cd0b6eb6722d1122ed2" }
bd-bonjson = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "693e1e433eb7e531a7817cd0b6eb6722d1122ed2" }
bd-buffer = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "693e1e433eb7e531a7817cd0b6eb6722d1122ed2" }
bd-client-common = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "693e1e433eb7e531a7817cd0b6eb6722d1122ed2" }
bd-client-stats-store = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "693e1e433eb7e531a7817cd0b6eb6722d1122ed2" }
bd-crash-handler = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "693e1e433eb7e531a7817cd0b6eb6722d1122ed2" }
bd-device = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "693e1e433eb7e531a7817cd0b6eb6722d1122ed2" }
bd-error-reporter = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "693e1e433eb7e531a7817cd0b6eb6722d1122ed2" }
bd-grpc = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "693e1e433eb7e531a7817cd0b6eb6722d1122ed2" }
bd-hyper-network = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "693e1e433eb7e531a7817cd0b6eb6722d1122ed2" }
bd-key-value = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "693e1e433eb7e531a7817cd0b6eb6722d1122ed2" }
bd-log = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "693e1e433eb7e531a7817cd0b6eb6722d1122ed2" }
bd-log-metadata = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "693e1e433eb7e531a7817cd0b6eb6722d1122ed2" }
bd-log-primitives = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "693e1e433eb7e531a7817cd0b6eb6722d1122ed2" }
bd-logger = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "693e1e433eb7e531a7817cd0b6eb6722d1122ed2" }
bd-noop-network = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "693e1e433eb7e531a7817cd0b6eb6722d1122ed2" }
bd-proto = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "693e1e433eb7e531a7817cd0b6eb6722d1122ed2" }
bd-report-writer = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "693e1e433eb7e531a7817cd0b6eb6722d1122ed2" }
bd-runtime = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "693e1e433eb7e531a7817cd0b6eb6722d1122ed2" }
bd-session = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "693e1e433eb7e531a7817cd0b6eb6722d1122ed2" }
bd-shutdown = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "693e1e433eb7e531a7817cd0b6eb6722d1122ed2" }
bd-test-helpers = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "693e1e433eb7e531a7817cd0b6eb6722d1122ed2", default-features = false }
bd-time = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "693e1e433eb7e531a7817cd0b6eb6722d1122ed2" }
bd-api = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "e8d4e18b6870bcf04d786fd3971c0f46d99ac802" }
bd-bonjson = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "e8d4e18b6870bcf04d786fd3971c0f46d99ac802" }
bd-buffer = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "e8d4e18b6870bcf04d786fd3971c0f46d99ac802" }
bd-client-common = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "e8d4e18b6870bcf04d786fd3971c0f46d99ac802" }
bd-client-stats-store = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "e8d4e18b6870bcf04d786fd3971c0f46d99ac802" }
bd-crash-handler = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "e8d4e18b6870bcf04d786fd3971c0f46d99ac802" }
bd-device = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "e8d4e18b6870bcf04d786fd3971c0f46d99ac802" }
bd-error-reporter = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "e8d4e18b6870bcf04d786fd3971c0f46d99ac802" }
bd-feature-flags = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "e8d4e18b6870bcf04d786fd3971c0f46d99ac802" }
bd-grpc = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "e8d4e18b6870bcf04d786fd3971c0f46d99ac802" }
bd-hyper-network = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "e8d4e18b6870bcf04d786fd3971c0f46d99ac802" }
bd-key-value = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "e8d4e18b6870bcf04d786fd3971c0f46d99ac802" }
bd-log = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "e8d4e18b6870bcf04d786fd3971c0f46d99ac802" }
bd-log-metadata = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "e8d4e18b6870bcf04d786fd3971c0f46d99ac802" }
bd-log-primitives = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "e8d4e18b6870bcf04d786fd3971c0f46d99ac802" }
bd-logger = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "e8d4e18b6870bcf04d786fd3971c0f46d99ac802" }
bd-noop-network = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "e8d4e18b6870bcf04d786fd3971c0f46d99ac802" }
bd-proto = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "e8d4e18b6870bcf04d786fd3971c0f46d99ac802" }
bd-report-writer = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "e8d4e18b6870bcf04d786fd3971c0f46d99ac802" }
bd-runtime = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "e8d4e18b6870bcf04d786fd3971c0f46d99ac802" }
bd-session = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "e8d4e18b6870bcf04d786fd3971c0f46d99ac802" }
bd-shutdown = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "e8d4e18b6870bcf04d786fd3971c0f46d99ac802" }
bd-test-helpers = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "e8d4e18b6870bcf04d786fd3971c0f46d99ac802", default-features = false }
bd-time = { git = "https://github.com/bitdriftlabs/shared-core.git", rev = "e8d4e18b6870bcf04d786fd3971c0f46d99ac802" }
clap = { version = "4.5.47", features = ["derive", "env"] }
ctor = "0.5.0"
env_logger = { version = "0.11.8", default-features = false }
Expand Down
2 changes: 2 additions & 0 deletions platform/shared/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ version = "1.0.0"
[dependencies]
anyhow.workspace = true
bd-api.workspace = true
bd-bonjson.workspace = true
bd-client-common.workspace = true
bd-client-stats-store.workspace = true
bd-error-reporter.workspace = true
bd-feature-flags.workspace = true
bd-log-primitives.workspace = true
bd-logger.workspace = true
bd-runtime.workspace = true
Expand Down
121 changes: 121 additions & 0 deletions platform/shared/src/feature_flags.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// capture-sdk - bitdrift's client SDK
// Copyright Bitdrift, Inc. All rights reserved.
//
// Use of this source code is governed by a source available license that can be found in the
// LICENSE file or at:
// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt

use bd_feature_flags::FeatureFlags;

crate::ffi_id_for!(FeatureFlagsHolder, FeatureFlagsId);

//
// FeatureFlagsHolder
//

/// A wrapper around the feature flags. The platform code passes a pointer to this
/// struct as the feature flags ID, allowing the ffi functions to cast this to the correct type and access
/// these fields to support the feature flags API.
pub struct FeatureFlagsHolder {
feature_flags: FeatureFlags,
}

crate::impl_holder_deref!(FeatureFlagsHolder, feature_flags, FeatureFlags);
crate::impl_holder_into_raw!(FeatureFlagsHolder, FeatureFlagsId);

impl FeatureFlagsHolder {
#[must_use]
pub const fn new(feature_flags: FeatureFlags) -> Self {
Self { feature_flags }
}

/// Given a valid feature flags ID, destroys the feature flags holder and frees the memory associated with it.
///
/// # Safety
/// The provided id *must* correspond to the pointer of a valid `FeatureFlagsHolder` as returned by
/// `into_raw`. This function *cannot* be called multiple times for the same id.
pub unsafe fn destroy(id: i64) {
let holder = Box::from_raw(id as *mut Self);
drop(holder);
}

/// Retrieves a feature flag by name.
///
/// Returns the feature flag if it exists, or `None` if no flag
/// with the given name is found.
///
/// # Arguments
///
/// * `key` - The name of the feature flag to retrieve
#[must_use]
pub fn get(&self, key: &str) -> Option<bd_feature_flags::FeatureFlag> {
self.feature_flags.get(key)
}

/// Sets or updates a feature flag.
///
/// Creates a new feature flag with the given name and variant, or updates an existing flag.
/// The flag is immediately stored in persistent storage and receives
/// a timestamp indicating when it was last modified.
///
/// # Arguments
///
/// * `key` - The name of the feature flag to set or update
/// * `variant` - The variant value for the flag:
/// - `Some(string)` sets the flag with the specified variant
/// - `None` sets the flag without a variant (simple boolean-style flag)
///
/// # Returns
///
/// Returns `Ok(())` on success, or an error if the flag cannot be stored.
pub fn set(&mut self, key: &str, variant: Option<&str>) -> anyhow::Result<()> {
self.feature_flags.set(key, variant)
}

/// Removes all feature flags from persistent storage.
///
/// This method deletes all feature flags, clearing the persistent storage.
/// This operation cannot be undone.
///
/// # Returns
///
/// Returns `Ok(())` on success, or an error if the storage cannot be cleared.
pub fn clear(&mut self) -> anyhow::Result<()> {
self.feature_flags.clear()
}

/// Synchronizes in-memory changes to persistent storage.
///
/// This method ensures that all feature flag changes made since the last sync
/// are written to disk. While changes are typically persisted automatically,
/// calling this method guarantees that data is flushed to storage immediately.
///
/// # Returns
///
/// Returns `Ok(())` on successful synchronization, or an error if the write operation fails.
pub fn sync(&self) -> anyhow::Result<()> {
self.feature_flags.sync()
}

/// Returns a `HashMap` containing all feature flags.
///
/// This method provides access to all feature flags as a standard
/// Rust `HashMap`. This is useful for iterating over all flags or performing
/// bulk operations. The `HashMap` is generated on-demand from the persistent storage.
///
/// # Returns
///
/// A `HashMap<String, FeatureFlag>` containing all flags.
#[must_use]
pub fn as_hashmap(&self) -> std::collections::HashMap<String, bd_feature_flags::FeatureFlag> {
self.feature_flags.as_hashmap()
}

/// Returns a reference to the underlying key-value store's `HashMap`,
/// allowing direct access to the raw stored values.
/// This should only be used internally to avoid unnecessary cloning.
#[must_use]
pub fn underlying_hashmap(&self) -> &std::collections::HashMap<String, bd_bonjson::Value> {
self.feature_flags.underlying_hashmap()
}
}
123 changes: 123 additions & 0 deletions platform/shared/src/ffi_wrapper.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// capture-sdk - bitdrift's client SDK
// Copyright Bitdrift, Inc. All rights reserved.
//
// Use of this source code is governed by a source available license that can be found in the
// LICENSE file or at:
// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt

use std::ops::{Deref, DerefMut};

/// A generic FFI ID type that provides a type-safe wrapper around i64 pointers
/// for passing objects across FFI boundaries.
///
/// This provides lifetime safety by preventing the ID from being sent across
/// thread boundaries when used as `FfiId<'_, T>` (note that `FfiId<'static, T>`
/// bypasses this protection).
#[repr(transparent)]
pub struct FfiId<'a, T> {
value: i64,
// A fake lifetime to allow us to link a non-static lifetime to the type, which allows us to
// limit its usage outside of the current function scope.
_lifetime: std::marker::PhantomData<&'a ()>,
// Phantom data to ensure the type parameter is part of the type system
_type: std::marker::PhantomData<T>,
}

impl<T> FfiId<'_, T> {
/// Creates a new `FfiId` from a raw pointer to a holder object. Use this in cases where you
/// need a manual conversion from an i64 to an `FfiId`.
///
/// # Safety
/// The provided pointer *must* be a valid pointer to a holder object of type T.
#[must_use]
pub const unsafe fn from_raw(value: i64) -> Self {
Self {
value,
_lifetime: std::marker::PhantomData,
_type: std::marker::PhantomData,
}
}

/// Returns a reference to the holder object that this `FfiId` represents.
///
/// # Safety
/// This is safe because all instances of `FfiId` are assumed to wrap a valid holder object.
const fn holder(&self) -> &T {
unsafe { &*(self.value as *const T) }
}

/// Returns a mutable reference to the holder object that this `FfiId` represents.
fn holder_mut(&mut self) -> &mut T {
unsafe { &mut *(self.value as *mut T) }
}
}

impl<T> Deref for FfiId<'_, T> {
type Target = T;

fn deref(&self) -> &Self::Target {
self.holder()
}
}

impl<T> DerefMut for FfiId<'_, T> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.holder_mut()
}
}

impl<T> From<FfiId<'_, T>> for i64 {
fn from(id: FfiId<'_, T>) -> Self {
id.value
}
}

/// Convenience macro for creating type-specific FFI ID aliases.
#[macro_export]
macro_rules! ffi_id_for {
($holder_type:ty, $id_type:ident) => {
pub type $id_type<'a> = $crate::ffi_wrapper::FfiId<'a, $holder_type>;
};
}

/// Convenience macro for implementing `Deref` and `DerefMut` for holder types.
///
/// This generates both `Deref` and `DerefMut` implementations that forward to a specific field.
#[macro_export]
macro_rules! impl_holder_deref {
($holder_type:ty, $field_name:ident, $target_type:ty) => {
impl std::ops::Deref for $holder_type {
type Target = $target_type;

fn deref(&self) -> &Self::Target {
&self.$field_name
}
}

impl std::ops::DerefMut for $holder_type {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.$field_name
}
}
};
}

/// Convenience macro for implementing `into_raw` method for holder types.
///
/// This generates an `into_raw` method that converts the holder into its corresponding
/// FFI ID type, following the standard pattern of boxing the holder and converting
/// the pointer to an i64.
#[macro_export]
macro_rules! impl_holder_into_raw {
($holder_type:ty, $id_type:ident) => {
impl $holder_type {
/// Consumes the holder and returns the raw pointer to it as an FFI ID. This effectively leaks the object, so
/// in order to avoid leaks the caller must ensure that the `destroy` is called with the returned
/// value.
#[must_use]
pub fn into_raw<'a>(self) -> $id_type<'a> {
unsafe { $id_type::from_raw(Box::into_raw(Box::new(self)) as i64) }
}
}
};
}
Loading
Loading