fireflyclient/README.md
Jacob Schmidt 0743b6c7f1
Some checks failed
Build / build (push) Failing after 1m14s
feat(client): Remove pipeline operations from FireflyClientPy
This commit removes the pipeline-related function definitions from the `FireflyClientPy` class in the `README.md` file. These functions are no longer needed in the Python client as pipeline operations are handled differently.

The removed functions include:

- `SetPipelineMode`
- `SetBatchSize`
- `FlushPipeline`
- `GetQueuedCommandCount`
- `IsPipelineMode`
- `GetBatchSize`
2025-04-13 17:07:38 -05:00

608 lines
24 KiB
Markdown

# 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<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 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/<RID>/publish`)*
### Usage (C/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)
```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.