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:
# 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:
- Add a reference to the compiled
FireflyClient.dll
. - Or add a project reference if working in the same solution.
Basic Usage
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<string> 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<string, string> 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<string, string> { { "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 byusing
).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
).
# 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/<RID>/publish
)
Usage (C/C++)
#include "firefly.h" // Make sure this path is correct
#include <stdio.h>
#include <stdlib.h> // For exit
#include <string.h> // 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)
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 anychar*
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 byCreateClient
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
-
Memory Management: Always free returned strings.
char* value = StringGet(client, "mykey"); if (value) { // Use value printf("Value: %s\n", value); FreeString(value); // Essential! }
-
Error Handling: Check return values.
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); }
-
Resource Cleanup: Always destroy the client.
void* client = CreateClient(...); // ... Use client ... DestroyClient(client); // Ensure cleanup
-
Encoding: Assume all
char*
parameters and return values use UTF-8 encoding.