Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
#include <launchdarkly/bindings/c/context.h>
#include <launchdarkly/bindings/c/data/evaluation_detail.h>
#include <launchdarkly/bindings/c/export.h>
#include <launchdarkly/bindings/c/listener_connection.h>
#include <launchdarkly/bindings/c/memory_routines.h>
#include <launchdarkly/bindings/c/status.h>
#include <launchdarkly/bindings/c/value.h>

#include <stddef.h>

#ifdef __cplusplus
Expand Down Expand Up @@ -381,6 +383,74 @@ LDClientSDK_JsonVariationDetail(LDClientSDK sdk,
*/
LD_EXPORT(void) LDClientSDK_Free(LDClientSDK sdk);

typedef void (*FlagChangedCallbackFn)(char const* flag_key,
LDValue new_value,
LDValue old_value,
bool deleted,
void* user_data);

/**
* Defines a feature flag listener which may be used to listen for flag changes.
* The struct should be initialized using LDFlagListener_Init before use.
*/
struct LDFlagListener {
/**
* Callback function which is invoked for flag changes.
*
* The provided pointers are only valid for the duration of the function
* call.
*
* @param flag_key The name of the flag that changed.
* @param new_value The new value of the flag. If there was not an new
* value, because the flag was deleted, then the LDValue will be of a null
* type. Check the deleted parameter to see if a flag was deleted.
* @param old_value The old value of the flag. If there was not an old
* value, for instance a newly created flag, then the Value will be of a
* null type.
* @param deleted True if the flag has been deleted.
*/
FlagChangedCallbackFn FlagChanged;

/**
* UserData is forwarded into callback functions.
*/
void* UserData;
};

/**
* Initializes a flag listener. Must be called before passing the listener
* to LDClientSDK_FlagNotifier_OnFlagChange.
*
* Create the struct, initialize the struct, set the FlagChanged handler
* and optionally UserData, and then pass the struct to
* LDClientSDK_FlagNotifier_OnFlagChange.
*
* @param listener Listener to initialize.
*/
LD_EXPORT(void) LDFlagListener_Init(struct LDFlagListener listener);

/**
* Listen for changes for the specific flag.
*
* If the FlagChanged member of the listener struct is not set (NULL), then the
* function will not register a listener. In that case the return value
* will be NULL.
*
* @param sdk SDK. Must not be NULL.
* @param flag_key The unique key for the feature flag. Must not be NULL.
* @param listener The listener, whose FlagChanged callback will be invoked,
* when the flag changes. Must not be NULL.
*
* @return A LDListenerConnection. The connection can be freed using
* LDListenerConnection_Free and the listener can be disconnected using
* LDListenerConnection_Disconnect. NULL will be returned if the FlagChanged
* member of the listener struct is NULL.
*/
LD_EXPORT(LDListenerConnection)
LDClientSDK_FlagNotifier_OnFlagChange(LDClientSDK sdk,
char const* flag_key,
struct LDFlagListener listener);

#ifdef __cplusplus
}
#endif
Expand Down
32 changes: 32 additions & 0 deletions libs/client-sdk/src/bindings/c/sdk.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -296,5 +296,37 @@ LD_EXPORT(void) LDClientSDK_Free(LDClientSDK sdk) {
delete TO_SDK(sdk);
}

LD_EXPORT(LDListenerConnection)
LDClientSDK_FlagNotifier_OnFlagChange(LDClientSDK sdk,
char const* flag_key,
struct LDFlagListener listener) {
LD_ASSERT_NOT_NULL(sdk);
LD_ASSERT_NOT_NULL(flag_key);

if (listener.FlagChanged) {
auto connection = TO_SDK(sdk)->FlagNotifier().OnFlagChange(
flag_key,
[listener](std::shared_ptr<launchdarkly::client_side::flag_manager::
FlagValueChangeEvent> event) {
listener.FlagChanged(
event->FlagName().c_str(),
reinterpret_cast<LDValue>(
const_cast<Value*>(&event->NewValue())),
reinterpret_cast<LDValue>(
const_cast<Value*>(&event->OldValue())),
event->Deleted(), listener.UserData);
});

return reinterpret_cast<LDListenerConnection>(connection.release());
}

return nullptr;
}

LD_EXPORT(void) LDFlagListener_Init(struct LDFlagListener listener) {
listener.FlagChanged = nullptr;
listener.UserData = nullptr;
}

// NOLINTEND cppcoreguidelines-pro-type-reinterpret-cast
// NOLINTEND OCInconsistentNamingInspection
41 changes: 41 additions & 0 deletions libs/client-sdk/tests/client_c_bindings_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,44 @@ TEST(ClientBindings, MinimalInstantiation) {

LDClientSDK_Free(sdk);
}

void ListenerFunction(char const* flag_key,
LDValue new_value,
LDValue old_value,
bool deleted,
void* user_data) {
}

// This test registers a listener. It doesn't use the listener, but it
// will at least ensure 1.) Compilation, and 2.) Allow sanitizers to run.
TEST(ClientBindings, RegisterFlagListener) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we do TestData for the server possibly we can add something similar to the client. It would make legitimate testing of these much simpler.

On the C++ side it is better tested because we can directly make the updater.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did test this a number of ways in the hello-c app.

LDClientConfigBuilder cfg_builder = LDClientConfigBuilder_New("sdk-123");
LDClientConfigBuilder_Offline(cfg_builder, true);

LDClientConfig config;
LDStatus status = LDClientConfigBuilder_Build(cfg_builder, &config);
ASSERT_TRUE(LDStatus_Ok(status));

LDContextBuilder ctx_builder = LDContextBuilder_New();
LDContextBuilder_AddKind(ctx_builder, "user", "shadow");

LDContext context = LDContextBuilder_Build(ctx_builder);

LDClientSDK sdk = LDClientSDK_New(config, context);

bool success = false;
LDClientSDK_Start(sdk, 3000, &success);
EXPECT_TRUE(success);

struct LDFlagListener listener{};
LDFlagListener_Init(listener);
listener.UserData = const_cast<char*>("Potato");
listener.FlagChanged = ListenerFunction;

LDListenerConnection connection = LDClientSDK_FlagNotifier_OnFlagChange(sdk, "my-boolean-flag", listener);

LDListenerConnection_Disconnect(connection);

LDListenerConnection_Free(connection);
LDClientSDK_Free(sdk);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// NOLINTBEGIN modernize-use-using

#pragma once

#include <launchdarkly/bindings/c/export.h>

#ifdef __cplusplus
extern "C" { // only need to export C interface if
// used by C++ source code
#endif

/**
* Handle that represents a listener connection.
*
* To stop unregister a listener call LDListenerConnection_Disconnect.
* To free a connection listener call LDListenerConnection_Free.
*
* Freeing an LDListenerConnection does not disconnect the connection. If it is
* deleted, without being disconnected, then the listener will remain active
* until the associated SDK is freed.
*/
typedef struct _LDListenerConnection* LDListenerConnection;

/**
* Disconnect a listener.
*
* @param connection The connection for the listener to disconnect.
* Must not be NULL.
*/
LD_EXPORT(void) LDListenerConnection_Disconnect(LDListenerConnection connection);

/**
* Free a listener connection.
*
* @param connection The LDListenerConnection to free.
*/
LD_EXPORT(void) LDListenerConnection_Free(LDListenerConnection connection);

#ifdef __cplusplus
}
#endif
2 changes: 1 addition & 1 deletion libs/common/include/launchdarkly/connection.hpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#pragma once

namespace launchdarkly::client_side {
namespace launchdarkly {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was moved to common, but client_side was not removed.


/**
* Represents the connection of a listener.
Expand Down
3 changes: 2 additions & 1 deletion libs/common/src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ add_library(${LIBNAME} OBJECT
config/persistence_builder.cpp
bindings/c/data/evaluation_detail.cpp
bindings/c/memory_routines.cpp
config/logging_builder.cpp)
config/logging_builder.cpp
bindings/c/listener_connection.cpp)

add_library(launchdarkly::common ALIAS ${LIBNAME})

Expand Down
18 changes: 18 additions & 0 deletions libs/common/src/bindings/c/listener_connection.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#include <launchdarkly/bindings/c/listener_connection.h>
#include <launchdarkly/detail/c_binding_helpers.hpp>

#include <launchdarkly/connection.hpp>

#include <memory>

#define TO_LC(ptr) (reinterpret_cast<launchdarkly::IConnection*>(ptr))

LD_EXPORT(void)
LDListenerConnection_Disconnect(LDListenerConnection connection) {
LD_ASSERT_NOT_NULL(connection);
TO_LC(connection)->Disconnect();
}

LD_EXPORT(void) LDListenerConnection_Free(LDListenerConnection connection) {
delete TO_LC(connection);
}