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

24 KiB

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:

  1. Add a reference to the compiled FireflyClient.dll.
  2. 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 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).

# 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 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.

    char* value = StringGet(client, "mykey");
    if (value) {
        // Use value
        printf("Value: %s\n", value);
        FreeString(value); // Essential!
    }
    
  2. 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);
    }
    
  3. Resource Cleanup: Always destroy the client.

    void* client = CreateClient(...);
    // ... Use client ...
    DestroyClient(client); // Ensure cleanup
    
  4. Encoding: Assume all char* parameters and return values use UTF-8 encoding.