# Firefly Client Library A C# client library for interacting with the Firefly Redis-compatible server. This library provides a clean API for storing and retrieving data from Firefly servers, handling all the protocol details for you. ## Features - Simple API for common Redis/Firefly operations (strings, lists, hashes) - Automatic handling of quotes and special characters in commands - Built-in connection management and authentication - Optional Native Interop library for use from C, C++, Python, etc. - Designed with thread-safety in mind (see Thread Safety section) ## Usage ### As a Console Application Simply run the FireflyClient executable (if built as an executable) to connect to a Firefly server and execute commands interactively: ```bash # Example (replace FireflyClient.exe with actual executable name/path) ./FireflyClient --host 127.0.0.1 --port 6379 --password yourpassword ``` ### As a C# Library #### Installation Reference the FireflyClient library in your .NET project: 1. Add a reference to the compiled `FireflyClient.dll`. 2. Or add a project reference if working in the same solution. #### Basic Usage ```csharp using FireflyClient; // Namespace using System; using System.Collections.Generic; // Create a client within a using statement for proper disposal // Connects to localhost:6379 by default using (var client = new FireflyClient("127.0.0.1", 6379)) { try { // Authenticate if needed // bool authResult = client.Authenticate("yourpassword"); // if (!authResult) { // Console.WriteLine("Authentication failed."); // return; // } // Store a string client.StringSet("mykey", "Hello C# world!"); // Retrieve a string string value = client.StringGet("mykey"); Console.WriteLine($"GET mykey: {value}"); // Work with lists client.ListRightPush("mylist", "item1"); // Can take multiple values client.ListRightPush("mylist", "item2", "item3"); List items = client.ListRange("mylist", 0, -1); Console.WriteLine($"LRANGE mylist: {string.Join(", ", items)}"); // Work with hashes client.HashSet("user:1", "name", "Alice"); client.HashSet("user:1", "email", "alice@example.com"); Dictionary userData = client.HashGetAll("user:1"); Console.WriteLine("HGETALL user:1:"); foreach(var kvp in userData) { Console.WriteLine($" {kvp.Key}: {kvp.Value}"); } // Use HMSET var moreData = new Dictionary { { "city", "Csharpville" }, { "zip", "98765" } }; client.HashMultiSet("user:1", moreData); Console.WriteLine($"HMSET city/zip for user:1"); string city = client.HashGet("user:1", "city"); Console.WriteLine($"HGET user:1 city: {city}"); // Delete keys client.Delete("mykey"); client.Delete("mylist"); client.Delete("user:1"); Console.WriteLine("Deleted keys."); } catch (Exception ex) { Console.WriteLine($"An error occurred: {ex.Message}"); } } // client.Dispose() is automatically called here ``` #### Error Handling Operations can throw exceptions (e.g., `SocketException` for connection issues, `InvalidOperationException` for protocol errors). Always use try-catch blocks for robust applications. ## C# API Reference (Partial) This assumes the `FireflyClient` object is named `client`. ### Client Management - `new FireflyClient(host, port)`: Constructor to create and connect. - `new FireflyClient(host, port, password)`: Constructor to create, connect, and authenticate. - `client.Dispose()`: Disconnects and releases resources (called automatically by `using`). - `client.Authenticate(password)`: Authenticates an existing connection. - `client.IsConnected`: Property (bool) to check connection status. - `client.IsAuthenticated`: Property (bool) to check authentication status. ### String Operations - `client.StringSet(key, value)`: Sets a string value. - `client.StringGet(key)`: Gets a string value. - `client.Delete(key)`: Deletes a key (works for any type). ### List Operations - `client.ListLeftPush(key, value, ...)`: Inserts value(s) at the head. - `client.ListRightPush(key, value, ...)`: Appends value(s) to the tail. - `client.ListLeftPop(key)`: Removes and returns the first element. - `client.ListRightPop(key)`: Removes and returns the last element. - `client.ListRange(key, start, stop)`: Gets a range of elements. - `client.ListIndex(key, index)`: Gets element at index. - `client.ListSet(key, index, value)`: Sets element at index. - `client.ListPosition(key, element, rank, maxlen)`: Finds position of element. - `client.ListTrim(key, start, stop)`: Trims list to specified range. - `client.ListRemove(key, count, element)`: Removes elements from list. ### Hash Operations - `client.HashSet(key, field, value)`: Sets a field in a hash. - `client.HashGet(key, field)`: Gets a field from a hash. - `client.HashDelete(key, field)`: Deletes a field from a hash. - `client.HashFieldExists(key, field)`: Checks if a field exists. - `client.HashMultiSet(key, dictionary)`: Sets multiple fields from a Dictionary. - `client.HashGetAll(key)`: Gets all fields and values as a Dictionary. ### Pipeline Operations - `client.SetPipelineMode(enabled)`: Enables/disables pipeline mode. - `client.SetBatchSize(size)`: Sets the command batch size for pipelining. - `client.FlushPipeline()`: Sends all queued commands. - `client.QueuedCommandCount`: Property (int) to get queued command count. - `client.IsPipelineMode`: Property (bool) to check pipeline mode. - `client.MaxBatchSize`: Property (int) to get max batch size. ### Server Operations (Example - verify actual methods) - `client.ExecuteCommand(command, args)`: Executes a raw command. - `client.Ping()`: Checks server responsiveness. - `client.Save()`: Requests a synchronous save. - `client.BackgroundSave()`: Requests an asynchronous save. *(Note: This API list is based on common patterns and native functions. Verify actual C# method names and signatures.)* ## Native Interop Library (`firefly.h` API) A native C API is provided for using the client from non-.NET languages. ### Prerequisites - **Library Build:** You need the compiled shared library (`libFireflyClient.dll` on Windows, `libFireflyClient.so` on Linux/macOS). - **Header:** You need the `firefly.h` header file. - **Runtime:** The target machine needs the appropriate .NET Runtime installed (e.g., .NET 9.0 as specified during build). ### Building the Native Library Use `dotnet publish` with the appropriate Runtime Identifier (RID) and configuration. Example publish profiles might be provided (`NativeLinux`, `NativeWin`). ```bash # Example Linux Build dotnet publish -c Release -r linux-x64 --self-contained false # Requires .NET Runtime # Or self-contained (larger size): # dotnet publish -c Release -r linux-x64 --self-contained true # Example Windows Build dotnet publish -c Release -r win-x64 --self-contained false ``` *(Adjust RID and configuration as needed. Output will be in `bin/Release/net9.0//publish`)* ### Usage (C/C++) ```c #include "firefly.h" // Make sure this path is correct #include #include // For exit #include // For string processing // Simple helper to split string by newline void process_multiline_string(const char* str, const char* prefix) { if (!str) return; char* buffer = strdup(str); if (!buffer) return; char* line = strtok(buffer, "\n"); while (line != NULL) { printf("%s%s\n", prefix, line); line = strtok(NULL, "\n"); } free(buffer); } // Simple helper to parse field=value string void process_hash_string(const char* str, const char* prefix) { if (!str) return; char* buffer = strdup(str); if (!buffer) return; char* line = strtok(buffer, "\n"); while (line != NULL) { char* eq = strchr(line, '='); if (eq) { *eq = '\0'; // Split string at '=' printf("%s%s: %s\n", prefix, line, eq + 1); } line = strtok(NULL, "\n"); } free(buffer); } int main() { // Initialize client void* client = CreateClient("127.0.0.1", 6379); if (!client) { fprintf(stderr, "Failed to create client\n"); return 1; } printf("Client created.\n"); // Authenticate (optional) // if (!Authenticate(client, "your_password")) { ... } // String operations StringSet(client, "mykey", "Hello C world!"); char* value = StringGet(client, "mykey"); if (value) { printf("StringGet: %s\n", value); FreeString(value); // Free the returned string } // List operations ListRightPush(client, "mylist", "item1"); ListRightPush(client, "mylist", "item2"); // Get list range (char* return) char* range = ListRange(client, "mylist", 0, -1); if (range) { printf("ListRange:\n"); process_multiline_string(range, " - "); FreeString(range); // Free the returned string } // Use other list ops... ListSet(client, "mylist", 0, "item1_updated"); ListRemove(client, "mylist", 1, "item2"); // Hash operations HashSet(client, "user:1", "name", "Alice C"); HashMultiSet(client, "user:1", "email alice.c@example.com city C-Town"); char* name = HashGet(client, "user:1", "name"); if (name) { printf("HashGet Name: %s\n", name); FreeString(name); // Free the returned string } // Get all hash fields (char* return) char* hashData = HashGetAll(client, "user:1"); if (hashData) { printf("HashGetAll:\n"); process_hash_string(hashData, " "); FreeString(hashData); // Free the returned string } // Cleanup keys Delete(client, "mykey"); Delete(client, "mylist"); Delete(client, "user:1"); // Cleanup client printf("Destroying client.\n"); DestroyClient(client); return 0; } ``` ### Usage (Python) ```python import ctypes import os import platform from ctypes import c_char_p, c_int, c_void_p, c_bool # Use c_bool # --- Define FireflyClient Python Wrapper Class --- class FireflyClientPy: def __init__(self, library_path=None, host="127.0.0.1", port=6379, password=None): if library_path is None: # Simple auto-detect based on OS (adjust path as needed) script_dir = os.path.dirname(os.path.abspath(__file__)) if platform.system() == "Windows": library_path = os.path.join(script_dir, "libFireflyClient.dll") # Assumes DLL is here else: library_path = os.path.join(script_dir, "libFireflyClient.so") # Assumes SO is here if not os.path.exists(library_path): raise FileNotFoundError(f"Shared library not found: {library_path}") self.lib = ctypes.CDLL(library_path) self._define_signatures() self.client_handle = self.lib.CreateClient(host.encode('utf-8'), port) if not self.client_handle: raise ConnectionError(f"Failed to create client for {host}:{port}") if password: if not self.lib.Authenticate(self.client_handle, password.encode('utf-8')): self.lib.DestroyClient(self.client_handle) raise ConnectionError("Authentication failed") def _define_signatures(self): # Client Management self.lib.CreateClient.argtypes = [c_char_p, c_int] self.lib.CreateClient.restype = c_void_p self.lib.DestroyClient.argtypes = [c_void_p] self.lib.DestroyClient.restype = None self.lib.Authenticate.argtypes = [c_void_p, c_char_p] self.lib.Authenticate.restype = c_bool # String Ops self.lib.StringSet.argtypes = [c_void_p, c_char_p, c_char_p] self.lib.StringSet.restype = c_bool self.lib.StringGet.argtypes = [c_void_p, c_char_p] self.lib.StringGet.restype = c_char_p self.lib.Delete.argtypes = [c_void_p, c_char_p] self.lib.Delete.restype = c_int self.lib.FreeString.argtypes = [c_char_p] # Keep c_char_p, ctypes handles IntPtr self.lib.FreeString.restype = None # List Ops self.lib.ListLeftPush.argtypes = [c_void_p, c_char_p, c_char_p] self.lib.ListLeftPush.restype = c_int self.lib.ListRightPush.argtypes = [c_void_p, c_char_p, c_char_p] self.lib.ListRightPush.restype = c_int self.lib.ListLeftPop.argtypes = [c_void_p, c_char_p] self.lib.ListLeftPop.restype = c_char_p self.lib.ListRightPop.argtypes = [c_void_p, c_char_p] self.lib.ListRightPop.restype = c_char_p self.lib.ListRange.argtypes = [c_void_p, c_char_p, c_int, c_int] self.lib.ListRange.restype = c_char_p self.lib.ListIndex.argtypes = [c_void_p, c_char_p, c_int] self.lib.ListIndex.restype = c_char_p self.lib.ListSet.argtypes = [c_void_p, c_char_p, c_int, c_char_p] self.lib.ListSet.restype = c_bool self.lib.ListPosition.argtypes = [c_void_p, c_char_p, c_char_p, c_int, c_int] self.lib.ListPosition.restype = c_int self.lib.ListTrim.argtypes = [c_void_p, c_char_p, c_int, c_int] self.lib.ListTrim.restype = c_bool self.lib.ListRemove.argtypes = [c_void_p, c_char_p, c_int, c_char_p] self.lib.ListRemove.restype = c_int # Hash Ops self.lib.HashSet.argtypes = [c_void_p, c_char_p, c_char_p, c_char_p] self.lib.HashSet.restype = c_bool self.lib.HashGet.argtypes = [c_void_p, c_char_p, c_char_p] self.lib.HashGet.restype = c_char_p self.lib.HashDelete.argtypes = [c_void_p, c_char_p, c_char_p] self.lib.HashDelete.restype = c_bool self.lib.HashFieldExists.argtypes = [c_void_p, c_char_p, c_char_p] self.lib.HashFieldExists.restype = c_bool self.lib.HashMultiSet.argtypes = [c_void_p, c_char_p, c_char_p] self.lib.HashMultiSet.restype = c_bool self.lib.HashGetAll.argtypes = [c_void_p, c_char_p] self.lib.HashGetAll.restype = c_char_p # Raw Command self.lib.ExecuteCommand.argtypes = [c_void_p, c_char_p, c_char_p] self.lib.ExecuteCommand.restype = c_char_p def close(self): if self.client_handle: self.lib.DestroyClient(self.client_handle) self.client_handle = None def _call_get_string(self, func, key): key_b = key.encode('utf-8') result_ptr = func(self.client_handle, key_b) if result_ptr: value = ctypes.cast(result_ptr, c_char_p).value.decode('utf-8') self.lib.FreeString(result_ptr) return value return None def _parse_and_free_list(self, result_ptr): if result_ptr: full_string = ctypes.cast(result_ptr, c_char_p).value.decode('utf-8') self.lib.FreeString(result_ptr) return full_string.split('\n') if full_string else [] return [] def _parse_and_free_hash(self, result_ptr): data = {} if result_ptr: full_string = ctypes.cast(result_ptr, c_char_p).value.decode('utf-8') self.lib.FreeString(result_ptr) if full_string: pairs = full_string.split('\n') for pair in pairs: if '=' in pair: field, value = pair.split('=', 1) data[field] = value return data # --- Public Methods --- def string_set(self, key, value): return self.lib.StringSet(self.client_handle, key.encode('utf-8'), value.encode('utf-8')) def string_get(self, key): return self._call_get_string(self.lib.StringGet, key) def list_right_push(self, key, value): return self.lib.ListRightPush(self.client_handle, key.encode('utf-8'), value.encode('utf-8')) def list_range(self, key, start, stop): result_ptr = self.lib.ListRange(self.client_handle, key.encode('utf-8'), start, stop) return self._parse_and_free_list(result_ptr) def hash_set(self, key, field, value): return self.lib.HashSet(self.client_handle, key.encode('utf-8'), field.encode('utf-8'), value.encode('utf-8')) def hash_get_all(self, key): result_ptr = self.lib.HashGetAll(self.client_handle, key.encode('utf-8')) return self._parse_and_free_hash(result_ptr) # Add other methods here following the pattern... def list_left_pop(self, key): return self._call_get_string(self.lib.ListLeftPop, key) def hash_get(self, key, field): key_b = key.encode('utf-8') field_b = field.encode('utf-8') result_ptr = self.lib.HashGet(self.client_handle, key_b, field_b) if result_ptr: value = ctypes.cast(result_ptr, c_char_p).value.decode('utf-8') self.lib.FreeString(result_ptr) return value return None def hash_multi_set(self, key, field_value_dict): pairs_str = " ".join([f"{k} {v}" for k, v in field_value_dict.items()]) return self.lib.HashMultiSet(self.client_handle, key.encode('utf-8'), pairs_str.encode('utf-8')) def delete(self, key): return self.lib.Delete(self.client_handle, key.encode('utf-8')) def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): self.close() # --- Example Usage --- if __name__ == "__main__": try: # Assumes library is in the same directory or accessible via path # Use context manager for cleanup with FireflyClientPy(host="127.0.0.1", port=6379) as client: print("Python client created.") # Authenticate if needed: client.lib.Authenticate(...) after creation # String operations client.string_set("py:mykey", "Hello Python world!") value = client.string_get("py:mykey") print(f"StringGet: {value}") # List operations client.list_right_push("py:mylist", "item1") client.list_right_push("py:mylist", "item2") # Get list range (updated parsing) items = client.list_range("py:mylist", 0, -1) print(f"ListRange: {items}") # Use other list ops... left_popped = client.list_left_pop("py:mylist") print(f"ListLeftPop: {left_popped}") # Hash operations client.hash_set("py:user:1", "language", "Python") client.hash_multi_set("py:user:1", {"version": "3.10", "framework": "None"}) lang = client.hash_get("py:user:1", "language") print(f"HashGet language: {lang}") # Get all hash fields (updated parsing) hash_data = client.hash_get_all("py:user:1") print(f"HashGetAll: {hash_data}") # Cleanup client.delete("py:mykey") client.delete("py:mylist") client.delete("py:user:1") print("Cleaned up keys.") except (FileNotFoundError, ConnectionError, Exception) as e: print(f"Error: {e}") print("Python example finished.") ``` ## Native API Reference This lists the functions exported by the native library (defined in `firefly.h`). ### Client Management - `void* CreateClient(const char* host, int port)` - `void DestroyClient(void* handle)` - `bool Authenticate(void* handle, const char* password)` ### String Operations - `bool StringSet(void* handle, const char* key, const char* value)` - `char* StringGet(void* handle, const char* key)` - `int Delete(void* handle, const char* key)` ### List Operations - `int ListLeftPush(void* handle, const char* key, const char* value)` - `int ListRightPush(void* handle, const char* key, const char* value)` - `char* ListLeftPop(void* handle, const char* key)` - `char* ListRightPop(void* handle, const char* key)` - `char* ListRange(void* handle, const char* key, int start, int stop)` - `char* ListIndex(void* handle, const char* key, int index)` - `bool ListSet(void* handle, const char* key, int index, const char* value)` - `int ListPosition(void* handle, const char* key, const char* element, int rank, int maxlen)` - `bool ListTrim(void* handle, const char* key, int start, int stop)` - `int ListRemove(void* handle, const char* key, int count, const char* element)` ### Hash Operations - `bool HashSet(void* handle, const char* key, const char* field, const char* value)` - `char* HashGet(void* handle, const char* key, const char* field)` - `bool HashDelete(void* handle, const char* key, const char* field)` - `bool HashFieldExists(void* handle, const char* key, const char* field)` - `bool HashMultiSet(void* handle, const char* key, const char* fieldValuePairs)` - `char* HashGetAll(void* handle, const char* key)` ### Raw Command Execution - `char* ExecuteCommand(void* handle, const char* command, const char* args)` ### Pipeline Operations - `bool SetPipelineMode(void* handle, bool enabled)` - `bool SetBatchSize(void* handle, int size)` - `char* FlushPipeline(void* handle)` - `int GetQueuedCommandCount(void* handle)` - `bool IsPipelineMode(void* handle)` - `int GetBatchSize(void* handle)` ### Memory Management - `void FreeString(char* str)`: **Crucial** - Must be called by the user to free the memory for any `char*` returned by the library functions (StringGet, ListLeftPop, ListRightPop, ListRange, ListIndex, HashGet, HashGetAll, ExecuteCommand, FlushPipeline). ### Error Handling Most functions return `NULL` (for `char*`) or `false` / `0` / `-1` (for `bool`/`int`) on failure. The native caller should check these return values. ## Thread Safety The library's native API is designed with thread safety in mind, relying on the underlying thread-safe mechanisms implemented in the C# `FireflyClient`. - **Instance Handles (`void*`)**: Each handle returned by `CreateClient` represents an independent connection and state. Operations on different handles do not interfere. - **Internal Locking**: The C# implementation is assumed to use appropriate locking (e.g., per-key locks for hashes/lists, connection locks) to allow safe concurrent calls on the *same* handle from multiple native threads. - **Caller Responsibility**: The caller must ensure that a single handle is not concurrently used in conflicting ways if the underlying operation requires it (though many operations like reads on different keys should be safe). *(Verify the specific locking strategies implemented in the C# partial classes for detailed guarantees.)* ## Performance Considerations - **Interop Overhead**: There is inherent overhead in calling between native code and the .NET runtime. - **Pipelining**: Use the pipeline operations (`SetPipelineMode`, queue commands, `FlushPipeline`) for significantly better performance when sending multiple commands. - **Memory Management**: Frequent allocation/deallocation of strings across the interop boundary can impact performance. Ensure `FreeString` is called promptly. ## Best Practices 1. **Memory Management**: Always free returned strings. ```c char* value = StringGet(client, "mykey"); if (value) { // Use value printf("Value: %s\n", value); FreeString(value); // Essential! } ``` 2. **Error Handling**: Check return values. ```c if (!ListSet(client, key, index, value)) { // Check boolean return fprintf(stderr, "Error setting list value!\n"); } char* item = ListIndex(client, key, index); if (!item) { // Check pointer return fprintf(stderr, "Error getting list item or index out of range!\n"); } else { // Use item FreeString(item); } ``` 3. **Resource Cleanup**: Always destroy the client. ```c void* client = CreateClient(...); // ... Use client ... DestroyClient(client); // Ensure cleanup ``` 4. **Encoding**: Assume all `char*` parameters and return values use UTF-8 encoding.