From c975c3741448809faecbbf684e2d67f7f5db4a40 Mon Sep 17 00:00:00 2001 From: MuriloChianfa Date: Thu, 29 Jan 2026 01:15:58 -0300 Subject: [PATCH] add csharp bindings with pinvoke and nuget packaging support --- .github/workflows/ci.yml | 35 +- CMakeLists.txt | 85 ++++ bindings/csharp/.gitignore | 52 ++ bindings/csharp/CMakeLists.txt | 102 ++++ bindings/csharp/Dockerfile | 46 ++ .../csharp/LibLpm.Examples/BasicExample.cs | 210 ++++++++ .../csharp/LibLpm.Examples/BatchExample.cs | 234 +++++++++ .../LibLpm.Examples/LibLpm.Examples.csproj | 16 + bindings/csharp/LibLpm.Tests/BatchTests.cs | 287 +++++++++++ bindings/csharp/LibLpm.Tests/IPv4Tests.cs | 303 ++++++++++++ bindings/csharp/LibLpm.Tests/IPv6Tests.cs | 287 +++++++++++ .../csharp/LibLpm.Tests/LibLpm.Tests.csproj | 29 ++ bindings/csharp/LibLpm.Tests/ResourceTests.cs | 240 +++++++++ bindings/csharp/LibLpm.sln | 31 ++ bindings/csharp/LibLpm/LibLpm.csproj | 99 ++++ bindings/csharp/LibLpm/LpmAlgorithm.cs | 47 ++ bindings/csharp/LibLpm/LpmException.cs | 272 ++++++++++ bindings/csharp/LibLpm/LpmTrie.cs | 438 ++++++++++++++++ bindings/csharp/LibLpm/LpmTrieIPv4.cs | 328 ++++++++++++ bindings/csharp/LibLpm/LpmTrieIPv6.cs | 310 ++++++++++++ bindings/csharp/LibLpm/NativeLibraryLoader.cs | 369 ++++++++++++++ bindings/csharp/LibLpm/NativeMethods.cs | 456 +++++++++++++++++ bindings/csharp/LibLpm/SafeLpmHandle.cs | 85 ++++ bindings/csharp/README.md | 467 ++++++++++++++++++ docker/Dockerfile.csharp | 68 +++ docker/README.md | 36 ++ scripts/docker-build.sh | 7 +- 27 files changed, 4936 insertions(+), 3 deletions(-) create mode 100644 bindings/csharp/.gitignore create mode 100644 bindings/csharp/CMakeLists.txt create mode 100644 bindings/csharp/Dockerfile create mode 100644 bindings/csharp/LibLpm.Examples/BasicExample.cs create mode 100644 bindings/csharp/LibLpm.Examples/BatchExample.cs create mode 100644 bindings/csharp/LibLpm.Examples/LibLpm.Examples.csproj create mode 100644 bindings/csharp/LibLpm.Tests/BatchTests.cs create mode 100644 bindings/csharp/LibLpm.Tests/IPv4Tests.cs create mode 100644 bindings/csharp/LibLpm.Tests/IPv6Tests.cs create mode 100644 bindings/csharp/LibLpm.Tests/LibLpm.Tests.csproj create mode 100644 bindings/csharp/LibLpm.Tests/ResourceTests.cs create mode 100644 bindings/csharp/LibLpm.sln create mode 100644 bindings/csharp/LibLpm/LibLpm.csproj create mode 100644 bindings/csharp/LibLpm/LpmAlgorithm.cs create mode 100644 bindings/csharp/LibLpm/LpmException.cs create mode 100644 bindings/csharp/LibLpm/LpmTrie.cs create mode 100644 bindings/csharp/LibLpm/LpmTrieIPv4.cs create mode 100644 bindings/csharp/LibLpm/LpmTrieIPv6.cs create mode 100644 bindings/csharp/LibLpm/NativeLibraryLoader.cs create mode 100644 bindings/csharp/LibLpm/NativeMethods.cs create mode 100644 bindings/csharp/LibLpm/SafeLpmHandle.cs create mode 100644 bindings/csharp/README.md create mode 100644 docker/Dockerfile.csharp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 88c03ae..aaec907 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -115,6 +115,35 @@ jobs: run: | docker run --rm liblpm-go:ci + # C# bindings test + test-csharp-bindings: + name: Test C# Bindings + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + submodules: recursive + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build C# container + uses: docker/build-push-action@v6 + with: + context: . + file: docker/Dockerfile.csharp + push: false + load: true + tags: liblpm-csharp:ci + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Run C# tests + run: | + docker run --rm liblpm-csharp:ci + # Code quality checks code-quality: name: Code Quality @@ -167,7 +196,7 @@ jobs: ci-summary: name: CI Summary runs-on: ubuntu-latest - needs: [build-and-test, test-cpp-bindings, test-go-bindings, code-quality] + needs: [build-and-test, test-cpp-bindings, test-go-bindings, test-csharp-bindings, code-quality] if: always() steps: @@ -177,12 +206,14 @@ jobs: echo "Build and test: ${{ needs.build-and-test.result }}" echo "C++ bindings: ${{ needs.test-cpp-bindings.result }}" echo "Go bindings: ${{ needs.test-go-bindings.result }}" + echo "C# bindings: ${{ needs.test-csharp-bindings.result }}" echo "Code quality: ${{ needs.code-quality.result }}" # Fail if any required job failed if [[ "${{ needs.build-and-test.result }}" == "failure" ]] || \ [[ "${{ needs.test-cpp-bindings.result }}" == "failure" ]] || \ - [[ "${{ needs.test-go-bindings.result }}" == "failure" ]]; then + [[ "${{ needs.test-go-bindings.result }}" == "failure" ]] || \ + [[ "${{ needs.test-csharp-bindings.result }}" == "failure" ]]; then echo "One or more required jobs failed" exit 1 fi diff --git a/CMakeLists.txt b/CMakeLists.txt index 0c5b7da..a5e3da9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -61,6 +61,7 @@ option(WITH_DPDK_BENCHMARK "Build DPDK comparison benchmark (requires DPDK)" OFF option(WITH_EXTERNAL_LPM_BENCHMARK "Build benchmarks with external LPM libraries (Docker only)" OFF) option(BUILD_GO_WRAPPER "Build Go wrapper and bindings" OFF) option(BUILD_CPP_WRAPPER "Build C++ wrapper and bindings" OFF) +option(BUILD_CSHARP_WRAPPER "Build C# wrapper and bindings" OFF) # External LPM libraries directory (set by Docker builds) set(EXTERNAL_LPM_DIR "" CACHE PATH "Directory containing external LPM libraries") @@ -390,6 +391,85 @@ if(BUILD_GO_WRAPPER) endif() endif() +# C# wrapper +if(BUILD_CSHARP_WRAPPER) + find_program(DOTNET_EXECUTABLE dotnet) + if(DOTNET_EXECUTABLE) + # Get .NET version + execute_process( + COMMAND ${DOTNET_EXECUTABLE} --version + OUTPUT_VARIABLE DOTNET_VERSION + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + message(STATUS "Found .NET SDK: ${DOTNET_VERSION}") + + # Custom target to build C# wrapper + add_custom_target(csharp_wrapper ALL + COMMAND ${CMAKE_COMMAND} -E echo "Building C# wrapper..." + COMMAND ${DOTNET_EXECUTABLE} build --configuration Release + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/bindings/csharp + DEPENDS lpm + COMMENT "Building C# wrapper and bindings" + ) + + # Copy native library to runtimes directory after building + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + set(CSHARP_NATIVE_LIB_DIR "runtimes/linux-x64/native") + set(CSHARP_NATIVE_LIB_NAME "liblpm.so") + elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows") + set(CSHARP_NATIVE_LIB_DIR "runtimes/win-x64/native") + set(CSHARP_NATIVE_LIB_NAME "lpm.dll") + elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin") + set(CSHARP_NATIVE_LIB_DIR "runtimes/osx-x64/native") + set(CSHARP_NATIVE_LIB_NAME "liblpm.dylib") + endif() + + add_custom_command(TARGET csharp_wrapper POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory + ${CMAKE_CURRENT_SOURCE_DIR}/bindings/csharp/${CSHARP_NATIVE_LIB_DIR} + COMMAND ${CMAKE_COMMAND} -E copy_if_different + $ + ${CMAKE_CURRENT_SOURCE_DIR}/bindings/csharp/${CSHARP_NATIVE_LIB_DIR}/${CSHARP_NATIVE_LIB_NAME} + COMMENT "Copying native library to C# runtimes directory" + ) + + # Custom target to test C# wrapper + add_custom_target(csharp_test + COMMAND ${CMAKE_COMMAND} -E env "LD_LIBRARY_PATH=${CMAKE_BINARY_DIR}:$ENV{LD_LIBRARY_PATH}" + ${DOTNET_EXECUTABLE} test --configuration Release --no-build + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/bindings/csharp + DEPENDS csharp_wrapper + COMMENT "Testing C# wrapper" + ) + + # Custom target to pack NuGet package + add_custom_target(csharp_pack + COMMAND ${DOTNET_EXECUTABLE} pack --configuration Release -o ${CMAKE_CURRENT_BINARY_DIR}/nupkg + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/bindings/csharp + DEPENDS csharp_wrapper + COMMENT "Creating NuGet package" + ) + + # Custom target to run C# examples + add_custom_target(csharp_example + COMMAND ${CMAKE_COMMAND} -E env "LD_LIBRARY_PATH=${CMAKE_BINARY_DIR}:$ENV{LD_LIBRARY_PATH}" + ${DOTNET_EXECUTABLE} run --configuration Release --project LibLpm.Examples + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/bindings/csharp + DEPENDS csharp_wrapper + COMMENT "Running C# examples" + ) + + message(STATUS "C# wrapper targets added:") + message(STATUS " make csharp_wrapper - Build C# wrapper") + message(STATUS " make csharp_test - Run C# tests") + message(STATUS " make csharp_pack - Create NuGet package") + message(STATUS " make csharp_example - Run C# examples") + else() + message(WARNING ".NET SDK not found. C# wrapper will not be built.") + message(WARNING "Install .NET SDK to build the wrapper: https://dotnet.microsoft.com/download") + endif() +endif() + # Print configuration summary message(STATUS "liblpm configuration:") message(STATUS " Version: ${PROJECT_VERSION}") @@ -435,6 +515,11 @@ if(BUILD_CPP_WRAPPER) else() message(STATUS " Build C++ wrapper: OFF") endif() +if(BUILD_CSHARP_WRAPPER AND DOTNET_EXECUTABLE) + message(STATUS " Build C# wrapper: ON") +else() + message(STATUS " Build C# wrapper: OFF") +endif() # ============================================================================ # CPack Configuration for .deb and .rpm packages diff --git a/bindings/csharp/.gitignore b/bindings/csharp/.gitignore new file mode 100644 index 0000000..94f3d9d --- /dev/null +++ b/bindings/csharp/.gitignore @@ -0,0 +1,52 @@ +# Build results +[Dd]ebug/ +[Rr]elease/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio files +.vs/ +*.user +*.userosscache +*.sln.docstates + +# NuGet packages +*.nupkg +*.snupkg +nupkg/ + +# Test results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* +*.trx +TestResults/ + +# Coverage +*.coverage +*.coveragexml +coverage/ + +# JetBrains Rider +.idea/ + +# VS Code +.vscode/ + +# macOS +.DS_Store + +# Native libraries (populated during build) +# Don't ignore runtimes/ structure, but ignore actual binaries +runtimes/**/native/*.so +runtimes/**/native/*.dll +runtimes/**/native/*.dylib + +# Project-specific +*.received.* diff --git a/bindings/csharp/CMakeLists.txt b/bindings/csharp/CMakeLists.txt new file mode 100644 index 0000000..352d89f --- /dev/null +++ b/bindings/csharp/CMakeLists.txt @@ -0,0 +1,102 @@ +cmake_minimum_required(VERSION 3.16) + +# C# wrapper for liblpm +project(liblpm_csharp VERSION 2.0.0) + +# Find .NET SDK +find_program(DOTNET_EXECUTABLE dotnet) + +if(NOT DOTNET_EXECUTABLE) + message(FATAL_ERROR "dotnet not found. Install .NET SDK to build C# wrapper.") +endif() + +# Get .NET version +execute_process( + COMMAND ${DOTNET_EXECUTABLE} --version + OUTPUT_VARIABLE DOTNET_VERSION + OUTPUT_STRIP_TRAILING_WHITESPACE +) +message(STATUS "Found .NET SDK: ${DOTNET_VERSION}") + +# Find or import lpm library +if(NOT TARGET lpm) + set(LPM_ROOT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../..") + if(EXISTS "${LPM_ROOT_DIR}/include/lpm.h") + message(STATUS "Using liblpm from parent project") + else() + find_library(LPM_LIBRARY NAMES lpm) + if(LPM_LIBRARY) + message(STATUS "Found system liblpm: ${LPM_LIBRARY}") + else() + message(FATAL_ERROR "liblpm not found. Build the main project first or install liblpm.") + endif() + endif() +endif() + +# Configuration +set(CSHARP_BUILD_CONFIG "$,Debug,Release>") + +# Build C# wrapper +add_custom_target(csharp_wrapper ALL + COMMAND ${DOTNET_EXECUTABLE} build --configuration ${CSHARP_BUILD_CONFIG} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + DEPENDS lpm + COMMENT "Building C# wrapper" + VERBATIM +) + +# Test C# wrapper +add_custom_target(csharp_test + COMMAND ${DOTNET_EXECUTABLE} test --configuration ${CSHARP_BUILD_CONFIG} --no-build + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + DEPENDS csharp_wrapper + COMMENT "Testing C# wrapper" + VERBATIM +) + +# Pack NuGet package +add_custom_target(csharp_pack + COMMAND ${DOTNET_EXECUTABLE} pack --configuration Release -o ${CMAKE_CURRENT_BINARY_DIR}/nupkg + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + DEPENDS csharp_wrapper + COMMENT "Creating NuGet package" + VERBATIM +) + +# Run examples +add_custom_target(csharp_example + COMMAND ${DOTNET_EXECUTABLE} run --configuration ${CSHARP_BUILD_CONFIG} --project LibLpm.Examples + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + DEPENDS csharp_wrapper + COMMENT "Running C# examples" + VERBATIM +) + +# Copy native library to runtimes directory for local development +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + set(NATIVE_LIB_DIR "runtimes/linux-x64/native") + set(NATIVE_LIB_NAME "liblpm.so") +elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows") + set(NATIVE_LIB_DIR "runtimes/win-x64/native") + set(NATIVE_LIB_NAME "lpm.dll") +elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin") + set(NATIVE_LIB_DIR "runtimes/osx-x64/native") + set(NATIVE_LIB_NAME "liblpm.dylib") +endif() + +add_custom_command(TARGET csharp_wrapper POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_SOURCE_DIR}/${NATIVE_LIB_DIR} + COMMAND ${CMAKE_COMMAND} -E copy_if_different + $ + ${CMAKE_CURRENT_SOURCE_DIR}/${NATIVE_LIB_DIR}/${NATIVE_LIB_NAME} + COMMENT "Copying native library to runtimes directory" + VERBATIM +) + +message(STATUS "C# wrapper configuration:") +message(STATUS " .NET SDK: ${DOTNET_VERSION}") +message(STATUS " Build targets:") +message(STATUS " make csharp_wrapper - Build C# wrapper") +message(STATUS " make csharp_test - Run C# tests") +message(STATUS " make csharp_pack - Create NuGet package") +message(STATUS " make csharp_example - Run examples") diff --git a/bindings/csharp/Dockerfile b/bindings/csharp/Dockerfile new file mode 100644 index 0000000..9ea9711 --- /dev/null +++ b/bindings/csharp/Dockerfile @@ -0,0 +1,46 @@ +# Dockerfile for C# bindings CI testing +FROM mcr.microsoft.com/dotnet/sdk:8.0 + +# Install C build tools for native library compilation +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + gcc \ + cmake \ + ninja-build \ + git \ + pkg-config \ + libc6-dev \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /build + +# Copy the entire liblpm project +COPY . /build/ + +# Initialize submodules and build liblpm native library +RUN git config --global --add safe.directory /build && \ + if [ -f .gitmodules ]; then git submodule update --init --recursive; fi && \ + mkdir -p build && cd build && \ + cmake -DCMAKE_BUILD_TYPE=Release -GNinja .. && \ + ninja && \ + ninja install && \ + ldconfig + +# Copy native library to runtimes directory +RUN mkdir -p /build/bindings/csharp/runtimes/linux-x64/native && \ + cp /build/build/liblpm.so /build/bindings/csharp/runtimes/linux-x64/native/ + +# Build and test C# bindings +WORKDIR /build/bindings/csharp + +# Restore dependencies +RUN dotnet restore + +# Build in Release mode +RUN dotnet build --configuration Release --no-restore + +# Run tests +RUN dotnet test --configuration Release --no-build --verbosity normal + +# Default command: run tests +CMD ["dotnet", "test", "--configuration", "Release", "--verbosity", "normal"] diff --git a/bindings/csharp/LibLpm.Examples/BasicExample.cs b/bindings/csharp/LibLpm.Examples/BasicExample.cs new file mode 100644 index 0000000..0a103c7 --- /dev/null +++ b/bindings/csharp/LibLpm.Examples/BasicExample.cs @@ -0,0 +1,210 @@ +// BasicExample.cs - Basic usage example for liblpm C# bindings +// Demonstrates IPv4 and IPv6 LPM operations + +using System; +using System.Net; +using LibLpm; + +namespace LibLpm.Examples +{ + /// + /// Basic example demonstrating liblpm C# bindings. + /// + public static class BasicExample + { + public static void Main(string[] args) + { + Console.WriteLine("liblpm C# Bindings - Basic Example"); + Console.WriteLine("==================================="); + Console.WriteLine(); + + // Print library version + var version = LpmTrie.GetVersion(); + Console.WriteLine($"Library Version: {version}"); + Console.WriteLine(); + + // Run IPv4 examples + IPv4Example(); + Console.WriteLine(); + + // Run IPv6 examples + IPv6Example(); + Console.WriteLine(); + + // Run fast path example + FastPathExample(); + Console.WriteLine(); + + Console.WriteLine(new string('=', 60)); + Console.WriteLine(); + + // Run batch example + BatchExample.Run(); + Console.WriteLine(); + + Console.WriteLine("All examples completed successfully!"); + } + + /// + /// Demonstrates basic IPv4 operations. + /// + private static void IPv4Example() + { + Console.WriteLine("=== IPv4 Example ==="); + + // Create an IPv4 trie using the default algorithm (DIR-24-8) + using var trie = LpmTrieIPv4.CreateDefault(); + + // Add routes using different APIs + + // 1. String-based (convenient) + trie.Add("0.0.0.0/0", 1); // Default route + trie.Add("192.168.0.0/16", 100); // /16 prefix + trie.Add("192.168.1.0/24", 200); // /24 prefix + trie.Add("10.0.0.0/8", 300); // /8 prefix + trie.Add("172.16.0.0/12", 400); // /12 prefix + + // 2. Byte array (fast path, no parsing) + byte[] prefix = { 8, 8, 8, 0 }; + trie.Add(prefix, 24, 500); + + Console.WriteLine("Added routes:"); + Console.WriteLine(" 0.0.0.0/0 -> 1 (default)"); + Console.WriteLine(" 192.168.0.0/16 -> 100"); + Console.WriteLine(" 192.168.1.0/24 -> 200"); + Console.WriteLine(" 10.0.0.0/8 -> 300"); + Console.WriteLine(" 172.16.0.0/12 -> 400"); + Console.WriteLine(" 8.8.8.0/24 -> 500"); + Console.WriteLine(); + + // Perform lookups + Console.WriteLine("Lookups:"); + + // String-based lookup + var result1 = trie.Lookup("192.168.1.100"); + Console.WriteLine($" 192.168.1.100 -> {result1} (matches /24)"); + + var result2 = trie.Lookup("192.168.2.50"); + Console.WriteLine($" 192.168.2.50 -> {result2} (matches /16)"); + + // IPAddress lookup + var addr = IPAddress.Parse("10.255.255.255"); + var result3 = trie.Lookup(addr); + Console.WriteLine($" 10.255.255.255 -> {result3} (matches /8)"); + + // Byte array lookup (fastest) + byte[] lookupAddr = { 8, 8, 8, 8 }; + var result4 = trie.Lookup(lookupAddr); + Console.WriteLine($" 8.8.8.8 -> {result4} (matches /24)"); + + // Address with no specific route (falls back to default) + var result5 = trie.Lookup("1.2.3.4"); + Console.WriteLine($" 1.2.3.4 -> {result5} (matches default)"); + Console.WriteLine(); + + // Delete a route + Console.WriteLine("Deleting 192.168.1.0/24..."); + bool deleted = trie.Delete("192.168.1.0/24"); + Console.WriteLine($" Deleted: {deleted}"); + + var result6 = trie.Lookup("192.168.1.100"); + Console.WriteLine($" 192.168.1.100 -> {result6} (now matches /16)"); + } + + /// + /// Demonstrates basic IPv6 operations. + /// + private static void IPv6Example() + { + Console.WriteLine("=== IPv6 Example ==="); + + // Create an IPv6 trie using the default algorithm (Wide16) + using var trie = LpmTrieIPv6.CreateDefault(); + + // Add common IPv6 routes + trie.Add("::/0", 1); // Default route + trie.Add("2001:db8::/32", 100); // Documentation prefix + trie.Add("2001:db8:1::/48", 200); // More specific + trie.Add("fe80::/10", 300); // Link-local + trie.Add("fc00::/7", 400); // Unique local + trie.Add("ff00::/8", 500); // Multicast + + Console.WriteLine("Added routes:"); + Console.WriteLine(" ::/0 -> 1 (default)"); + Console.WriteLine(" 2001:db8::/32 -> 100"); + Console.WriteLine(" 2001:db8:1::/48 -> 200"); + Console.WriteLine(" fe80::/10 -> 300 (link-local)"); + Console.WriteLine(" fc00::/7 -> 400 (unique local)"); + Console.WriteLine(" ff00::/8 -> 500 (multicast)"); + Console.WriteLine(); + + // Perform lookups + Console.WriteLine("Lookups:"); + + var result1 = trie.Lookup("2001:db8:1::1"); + Console.WriteLine($" 2001:db8:1::1 -> {result1} (matches /48)"); + + var result2 = trie.Lookup("2001:db8:2::1"); + Console.WriteLine($" 2001:db8:2::1 -> {result2} (matches /32)"); + + var result3 = trie.Lookup("fe80::1"); + Console.WriteLine($" fe80::1 -> {result3} (link-local)"); + + var result4 = trie.Lookup("fd00::1"); + Console.WriteLine($" fd00::1 -> {result4} (unique local)"); + + var result5 = trie.Lookup("ff02::1"); + Console.WriteLine($" ff02::1 -> {result5} (multicast)"); + + var result6 = trie.Lookup("2607:f8b0:4004:800::200e"); + Console.WriteLine($" 2607:f8b0:4004:800::200e -> {result6} (matches default)"); + } + + /// + /// Demonstrates fast path operations with byte arrays. + /// + private static void FastPathExample() + { + Console.WriteLine("=== Fast Path Example ==="); + + using var trie = LpmTrieIPv4.CreateDefault(); + + // Fast path: Use byte arrays and uint directly + // This avoids string parsing overhead + + // Add routes using byte arrays + byte[] prefix1 = { 192, 168, 0, 0 }; + trie.Add(prefix1, 16, 100); + + byte[] prefix2 = { 10, 0, 0, 0 }; + trie.Add(prefix2, 8, 200); + + Console.WriteLine("Added routes using byte arrays"); + + // Fast lookup using uint (network byte order) + // 192.168.1.1 = 0xC0A80101 + uint addr1 = 0xC0A80101; + uint? result1 = trie.Lookup(addr1); + Console.WriteLine($" Lookup 0xC0A80101 -> {result1}"); + + // Even faster: LookupRaw returns raw value without nullable check + uint rawResult = trie.LookupRaw(addr1); + Console.WriteLine($" LookupRaw 0xC0A80101 -> {rawResult}"); + + // Use Span for zero-copy operations + Span spanPrefix = stackalloc byte[] { 172, 16, 0, 0 }; + trie.Add(spanPrefix, 12, 300); + + ReadOnlySpan spanAddr = stackalloc byte[] { 172, 31, 255, 255 }; + var result2 = trie.Lookup(spanAddr); + Console.WriteLine($" Span lookup 172.31.255.255 -> {result2}"); + + // Helper methods for conversion + uint converted = LpmTrieIPv4.ParseIPv4Address("192.168.1.1"); + Console.WriteLine($" ParseIPv4Address(\"192.168.1.1\") = 0x{converted:X8}"); + + byte[] bytes = LpmTrieIPv4.UInt32ToBytes(converted); + Console.WriteLine($" UInt32ToBytes(0x{converted:X8}) = [{string.Join(", ", bytes)}]"); + } + } +} diff --git a/bindings/csharp/LibLpm.Examples/BatchExample.cs b/bindings/csharp/LibLpm.Examples/BatchExample.cs new file mode 100644 index 0000000..2abc886 --- /dev/null +++ b/bindings/csharp/LibLpm.Examples/BatchExample.cs @@ -0,0 +1,234 @@ +// BatchExample.cs - Batch operations example for liblpm C# bindings +// Demonstrates high-performance batch lookups + +using System; +using System.Buffers.Binary; +using System.Diagnostics; +using LibLpm; + +namespace LibLpm.Examples +{ + /// + /// Example demonstrating batch lookup operations for high throughput. + /// + public static class BatchExample + { + /// + /// Runs the batch example. + /// + public static void Run() + { + Console.WriteLine("liblpm C# Bindings - Batch Operations Example"); + Console.WriteLine("============================================="); + Console.WriteLine(); + + IPv4BatchExample(); + Console.WriteLine(); + + IPv6BatchExample(); + Console.WriteLine(); + + PerformanceBenchmark(); + } + + /// + /// Demonstrates IPv4 batch lookups. + /// + private static void IPv4BatchExample() + { + Console.WriteLine("=== IPv4 Batch Lookup ==="); + + using var trie = LpmTrieIPv4.CreateDefault(); + + // Add some routes + trie.Add("192.168.0.0/16", 100); + trie.Add("10.0.0.0/8", 200); + trie.Add("172.16.0.0/12", 300); + trie.Add("0.0.0.0/0", 1); // Default route + + Console.WriteLine("Added routes: 192.168.0.0/16, 10.0.0.0/8, 172.16.0.0/12, 0.0.0.0/0"); + Console.WriteLine(); + + // Batch lookup with uint array + Console.WriteLine("Batch lookup with uint[] (network byte order):"); + uint[] addresses = new uint[] + { + 0xC0A80101, // 192.168.1.1 + 0xC0A80202, // 192.168.2.2 + 0x0A010101, // 10.1.1.1 + 0xAC101010, // 172.16.16.16 + 0x08080808, // 8.8.8.8 (no specific route) + }; + uint[] results = new uint[5]; + + trie.LookupBatch(addresses, results); + + for (int i = 0; i < addresses.Length; i++) + { + var addrStr = FormatIPv4(addresses[i]); + var resultStr = results[i] == LpmConstants.InvalidNextHop + ? "no match (default)" + : results[i].ToString(); + Console.WriteLine($" {addrStr} -> {resultStr}"); + } + Console.WriteLine(); + + // Batch lookup with byte array + Console.WriteLine("Batch lookup with byte[] (contiguous addresses):"); + byte[] byteAddresses = new byte[] + { + 192, 168, 1, 1, // Address 1 + 10, 255, 255, 1, // Address 2 + }; + uint[] byteResults = new uint[2]; + + trie.LookupBatch(byteAddresses, byteResults); + + Console.WriteLine($" 192.168.1.1 -> {byteResults[0]}"); + Console.WriteLine($" 10.255.255.1 -> {byteResults[1]}"); + Console.WriteLine(); + + // Batch lookup with Span (zero allocation) + Console.WriteLine("Batch lookup with Span (stack allocated):"); + Span spanAddresses = stackalloc uint[] + { + 0xC0A80303, // 192.168.3.3 + 0xC0A80404, // 192.168.4.4 + }; + Span spanResults = stackalloc uint[2]; + + trie.LookupBatch(spanAddresses, spanResults); + + Console.WriteLine($" 192.168.3.3 -> {spanResults[0]}"); + Console.WriteLine($" 192.168.4.4 -> {spanResults[1]}"); + } + + /// + /// Demonstrates IPv6 batch lookups. + /// + private static void IPv6BatchExample() + { + Console.WriteLine("=== IPv6 Batch Lookup ==="); + + using var trie = LpmTrieIPv6.CreateDefault(); + + // Add some routes + trie.Add("2001:db8::/32", 100); + trie.Add("2001:db8:1::/48", 200); + trie.Add("fe80::/10", 300); + trie.Add("::/0", 1); // Default route + + Console.WriteLine("Added routes: 2001:db8::/32, 2001:db8:1::/48, fe80::/10, ::/0"); + Console.WriteLine(); + + // Batch lookup with byte array + Console.WriteLine("Batch lookup with byte[] (3 addresses):"); + byte[] addresses = new byte[48]; // 3 addresses * 16 bytes each + + // Address 1: 2001:db8::1 + addresses[0] = 0x20; + addresses[1] = 0x01; + addresses[2] = 0x0d; + addresses[3] = 0xb8; + addresses[15] = 0x01; + + // Address 2: 2001:db8:1::1 + addresses[16] = 0x20; + addresses[17] = 0x01; + addresses[18] = 0x0d; + addresses[19] = 0xb8; + addresses[20] = 0x00; + addresses[21] = 0x01; + addresses[31] = 0x01; + + // Address 3: fe80::1 + addresses[32] = 0xfe; + addresses[33] = 0x80; + addresses[47] = 0x01; + + uint[] results = new uint[3]; + + trie.LookupBatch(addresses, results); + + Console.WriteLine($" 2001:db8::1 -> {results[0]} (matches /32)"); + Console.WriteLine($" 2001:db8:1::1 -> {results[1]} (matches /48)"); + Console.WriteLine($" fe80::1 -> {results[2]} (link-local)"); + } + + /// + /// Demonstrates performance of batch vs single lookups. + /// + private static void PerformanceBenchmark() + { + Console.WriteLine("=== Performance Comparison ==="); + Console.WriteLine(); + + using var trie = LpmTrieIPv4.CreateDefault(); + + // Add a default route + trie.Add("0.0.0.0/0", 1); + + const int count = 100000; + uint[] addresses = new uint[count]; + uint[] results = new uint[count]; + + // Generate random addresses + var random = new Random(42); + for (int i = 0; i < count; i++) + { + addresses[i] = (uint)random.Next(); + } + + // Warm up + trie.LookupBatch(addresses, results); + for (int i = 0; i < 1000; i++) + { + trie.Lookup(addresses[i]); + } + + // Benchmark batch lookup + var sw = Stopwatch.StartNew(); + for (int iter = 0; iter < 10; iter++) + { + trie.LookupBatch(addresses, results); + } + sw.Stop(); + double batchTime = sw.Elapsed.TotalMilliseconds / 10; + double batchRate = count / (batchTime / 1000.0); + + Console.WriteLine($"Batch lookup ({count:N0} addresses):"); + Console.WriteLine($" Time: {batchTime:F2} ms"); + Console.WriteLine($" Rate: {batchRate:N0} lookups/sec"); + Console.WriteLine(); + + // Benchmark single lookups + sw.Restart(); + for (int iter = 0; iter < 10; iter++) + { + for (int i = 0; i < count; i++) + { + trie.Lookup(addresses[i]); + } + } + sw.Stop(); + double singleTime = sw.Elapsed.TotalMilliseconds / 10; + double singleRate = count / (singleTime / 1000.0); + + Console.WriteLine($"Single lookups ({count:N0} addresses):"); + Console.WriteLine($" Time: {singleTime:F2} ms"); + Console.WriteLine($" Rate: {singleRate:N0} lookups/sec"); + Console.WriteLine(); + + double speedup = singleTime / batchTime; + Console.WriteLine($"Batch speedup: {speedup:F2}x"); + } + + /// + /// Formats a uint IPv4 address as a string. + /// + private static string FormatIPv4(uint addr) + { + return $"{(addr >> 24) & 0xFF}.{(addr >> 16) & 0xFF}.{(addr >> 8) & 0xFF}.{addr & 0xFF}"; + } + } +} diff --git a/bindings/csharp/LibLpm.Examples/LibLpm.Examples.csproj b/bindings/csharp/LibLpm.Examples/LibLpm.Examples.csproj new file mode 100644 index 0000000..d6f2619 --- /dev/null +++ b/bindings/csharp/LibLpm.Examples/LibLpm.Examples.csproj @@ -0,0 +1,16 @@ + + + + Exe + net8.0 + latest + enable + true + false + + + + + + + diff --git a/bindings/csharp/LibLpm.Tests/BatchTests.cs b/bindings/csharp/LibLpm.Tests/BatchTests.cs new file mode 100644 index 0000000..73c27e5 --- /dev/null +++ b/bindings/csharp/LibLpm.Tests/BatchTests.cs @@ -0,0 +1,287 @@ +// BatchTests.cs - Unit tests for batch LPM operations + +using System; +using System.Buffers.Binary; +using Xunit; + +namespace LibLpm.Tests +{ + /// + /// Tests for batch lookup operations. + /// + public class BatchTests + { + [Fact] + public void IPv4_LookupBatch_UInt32Array_Success() + { + using var trie = LpmTrieIPv4.CreateDefault(); + + trie.Add("192.168.0.0/16", 100); + trie.Add("10.0.0.0/8", 200); + + uint[] addresses = new uint[] + { + 0xC0A80101, // 192.168.1.1 + 0xC0A80202, // 192.168.2.2 + 0x0A010101, // 10.1.1.1 + 0x08080808, // 8.8.8.8 (no match) + }; + uint[] results = new uint[4]; + + trie.LookupBatch(addresses, results); + + Assert.Equal(100u, results[0]); + Assert.Equal(100u, results[1]); + Assert.Equal(200u, results[2]); + Assert.Equal(LpmConstants.InvalidNextHop, results[3]); + } + + [Fact] + public void IPv4_LookupBatch_Span_Success() + { + using var trie = LpmTrieIPv4.CreateDefault(); + + trie.Add("192.168.0.0/16", 100); + + Span addresses = stackalloc uint[] + { + 0xC0A80101, + 0xC0A80202, + }; + Span results = stackalloc uint[2]; + + trie.LookupBatch(addresses, results); + + Assert.Equal(100u, results[0]); + Assert.Equal(100u, results[1]); + } + + [Fact] + public void IPv4_LookupBatch_ByteArray_Success() + { + using var trie = LpmTrieIPv4.CreateDefault(); + + trie.Add("192.168.0.0/16", 100); + + // 2 addresses, 4 bytes each = 8 bytes total + byte[] addresses = new byte[] + { + 192, 168, 1, 1, // Address 1 + 192, 168, 2, 2, // Address 2 + }; + uint[] results = new uint[2]; + + trie.LookupBatch(addresses, results); + + Assert.Equal(100u, results[0]); + Assert.Equal(100u, results[1]); + } + + [Fact] + public void IPv4_LookupBatch_EmptyArray_Success() + { + using var trie = LpmTrieIPv4.CreateDefault(); + + uint[] addresses = Array.Empty(); + uint[] results = Array.Empty(); + + trie.LookupBatch(addresses, results); + // Should complete without error + } + + [Fact] + public void IPv4_LookupBatch_LargeArray_Success() + { + using var trie = LpmTrieIPv4.CreateDefault(); + + trie.Add("0.0.0.0/0", 999); + + const int count = 10000; + uint[] addresses = new uint[count]; + uint[] results = new uint[count]; + + // Fill with sequential addresses + for (int i = 0; i < count; i++) + { + addresses[i] = (uint)i; + } + + trie.LookupBatch(addresses, results); + + // All should match the default route + Assert.All(results, r => Assert.Equal(999u, r)); + } + + [Fact] + public void IPv4_LookupBatch_OutputTooSmall_ThrowsException() + { + using var trie = LpmTrieIPv4.CreateDefault(); + + uint[] addresses = new uint[10]; + uint[] results = new uint[5]; // Too small + + Assert.Throws(() => trie.LookupBatch(addresses, results)); + } + + [Fact] + public void IPv4_LookupBatch_InvalidByteArrayLength_ThrowsException() + { + using var trie = LpmTrieIPv4.CreateDefault(); + + byte[] addresses = new byte[7]; // Not a multiple of 4 + uint[] results = new uint[2]; + + Assert.Throws(() => trie.LookupBatch(addresses, results)); + } + + [Fact] + public void IPv6_LookupBatch_ByteArray_Success() + { + using var trie = LpmTrieIPv6.CreateDefault(); + + trie.Add("2001:db8::/32", 100); + + // 2 addresses, 16 bytes each = 32 bytes total + byte[] addresses = new byte[32]; + + // Address 1: 2001:db8::1 + addresses[0] = 0x20; + addresses[1] = 0x01; + addresses[2] = 0x0d; + addresses[3] = 0xb8; + addresses[15] = 0x01; + + // Address 2: 2001:db8::2 + addresses[16] = 0x20; + addresses[17] = 0x01; + addresses[18] = 0x0d; + addresses[19] = 0xb8; + addresses[31] = 0x02; + + uint[] results = new uint[2]; + + trie.LookupBatch(addresses, results); + + Assert.Equal(100u, results[0]); + Assert.Equal(100u, results[1]); + } + + [Fact] + public void IPv6_LookupBatch_Span_Success() + { + using var trie = LpmTrieIPv6.CreateDefault(); + + trie.Add("2001:db8::/32", 100); + + Span addresses = stackalloc byte[32]; + + // Address 1 + addresses[0] = 0x20; + addresses[1] = 0x01; + addresses[2] = 0x0d; + addresses[3] = 0xb8; + addresses[15] = 0x01; + + // Address 2 + addresses[16] = 0x20; + addresses[17] = 0x01; + addresses[18] = 0x0d; + addresses[19] = 0xb8; + addresses[31] = 0x02; + + Span results = stackalloc uint[2]; + + trie.LookupBatch(addresses, results); + + Assert.Equal(100u, results[0]); + Assert.Equal(100u, results[1]); + } + + [Fact] + public void IPv6_LookupBatch_EmptyArray_Success() + { + using var trie = LpmTrieIPv6.CreateDefault(); + + byte[] addresses = Array.Empty(); + uint[] results = Array.Empty(); + + trie.LookupBatch(addresses, results); + // Should complete without error + } + + [Fact] + public void IPv6_LookupBatch_InvalidByteArrayLength_ThrowsException() + { + using var trie = LpmTrieIPv6.CreateDefault(); + + byte[] addresses = new byte[20]; // Not a multiple of 16 + uint[] results = new uint[2]; + + Assert.Throws(() => trie.LookupBatch(addresses, results)); + } + + [Fact] + public void IPv6_LookupBatch_OutputTooSmall_ThrowsException() + { + using var trie = LpmTrieIPv6.CreateDefault(); + + byte[] addresses = new byte[32]; // 2 addresses + uint[] results = new uint[1]; // Too small + + Assert.Throws(() => trie.LookupBatch(addresses, results)); + } + + [Fact] + public void IPv6_LookupBatch_MixedMatches_Success() + { + using var trie = LpmTrieIPv6.CreateDefault(); + + trie.Add("2001:db8::/32", 100); + trie.Add("fc00::/7", 200); + + byte[] addresses = new byte[48]; // 3 addresses + + // Address 1: 2001:db8::1 (should match 100) + addresses[0] = 0x20; + addresses[1] = 0x01; + addresses[2] = 0x0d; + addresses[3] = 0xb8; + addresses[15] = 0x01; + + // Address 2: fd00::1 (should match 200) + addresses[16] = 0xfd; + addresses[31] = 0x01; + + // Address 3: 2001:db9::1 (no match) + addresses[32] = 0x20; + addresses[33] = 0x01; + addresses[34] = 0x0d; + addresses[35] = 0xb9; + addresses[47] = 0x01; + + uint[] results = new uint[3]; + + trie.LookupBatch(addresses, results); + + Assert.Equal(100u, results[0]); + Assert.Equal(200u, results[1]); + Assert.Equal(LpmConstants.InvalidNextHop, results[2]); + } + + [Fact] + public void IPv4_LookupBatch_OutputLargerThanInput_Success() + { + using var trie = LpmTrieIPv4.CreateDefault(); + + trie.Add("0.0.0.0/0", 100); + + uint[] addresses = new uint[] { 0x01020304 }; + uint[] results = new uint[10]; // Larger than needed + + trie.LookupBatch(addresses, results); + + Assert.Equal(100u, results[0]); + // Rest of results array is unspecified + } + } +} diff --git a/bindings/csharp/LibLpm.Tests/IPv4Tests.cs b/bindings/csharp/LibLpm.Tests/IPv4Tests.cs new file mode 100644 index 0000000..d85edb2 --- /dev/null +++ b/bindings/csharp/LibLpm.Tests/IPv4Tests.cs @@ -0,0 +1,303 @@ +// IPv4Tests.cs - Unit tests for IPv4 LPM operations + +using System; +using System.Net; +using Xunit; + +namespace LibLpm.Tests +{ + /// + /// Tests for IPv4 LPM trie operations. + /// + public class IPv4Tests + { + [Fact] + public void CreateDefault_ReturnsValidTrie() + { + using var trie = LpmTrieIPv4.CreateDefault(); + Assert.NotNull(trie); + Assert.False(trie.IsIPv6); + Assert.False(trie.IsDisposed); + } + + [Fact] + public void CreateDir24_ReturnsValidTrie() + { + using var trie = LpmTrieIPv4.CreateDir24(); + Assert.NotNull(trie); + Assert.Equal(LpmAlgorithm.Dir24, trie.Algorithm); + } + + [Fact] + public void CreateStride8_ReturnsValidTrie() + { + using var trie = LpmTrieIPv4.CreateStride8(); + Assert.NotNull(trie); + Assert.Equal(LpmAlgorithm.Stride8, trie.Algorithm); + } + + [Fact] + public void Create_WithWide16_ThrowsArgumentException() + { + Assert.Throws(() => LpmTrieIPv4.Create(LpmAlgorithm.Wide16)); + } + + [Fact] + public void AddAndLookup_ByteArray_Success() + { + using var trie = LpmTrieIPv4.CreateDefault(); + + byte[] prefix = { 192, 168, 0, 0 }; + trie.Add(prefix, 16, 100); + + byte[] addr = { 192, 168, 1, 1 }; + var result = trie.Lookup(addr); + + Assert.NotNull(result); + Assert.Equal(100u, result.Value); + } + + [Fact] + public void AddAndLookup_String_Success() + { + using var trie = LpmTrieIPv4.CreateDefault(); + + trie.Add("10.0.0.0/8", 200); + + var result = trie.Lookup("10.255.255.255"); + + Assert.NotNull(result); + Assert.Equal(200u, result.Value); + } + + [Fact] + public void AddAndLookup_IPAddress_Success() + { + using var trie = LpmTrieIPv4.CreateDefault(); + + trie.Add("172.16.0.0/12", 300); + + var addr = IPAddress.Parse("172.31.255.255"); + var result = trie.Lookup(addr); + + Assert.NotNull(result); + Assert.Equal(300u, result.Value); + } + + [Fact] + public void AddAndLookup_UInt32_Success() + { + using var trie = LpmTrieIPv4.CreateDefault(); + + byte[] prefix = { 192, 168, 0, 0 }; + trie.Add(prefix, 16, 100); + + // 192.168.1.1 in network byte order + uint addr = 0xC0A80101; + var result = trie.Lookup(addr); + + Assert.NotNull(result); + Assert.Equal(100u, result.Value); + } + + [Fact] + public void Lookup_NoMatch_ReturnsNull() + { + using var trie = LpmTrieIPv4.CreateDefault(); + + trie.Add("192.168.0.0/16", 100); + + var result = trie.Lookup("10.0.0.1"); + + Assert.Null(result); + } + + [Fact] + public void LookupRaw_NoMatch_ReturnsInvalidNextHop() + { + using var trie = LpmTrieIPv4.CreateDefault(); + + uint result = trie.LookupRaw(0x0A000001); // 10.0.0.1 + + Assert.Equal(LpmConstants.InvalidNextHop, result); + } + + [Fact] + public void Delete_ExistingPrefix_ReturnsTrue() + { + using var trie = LpmTrieIPv4.CreateDefault(); + + trie.Add("192.168.0.0/16", 100); + + bool deleted = trie.Delete("192.168.0.0/16"); + + Assert.True(deleted); + Assert.Null(trie.Lookup("192.168.1.1")); + } + + [Fact] + public void Delete_NonExistingPrefix_ReturnsFalse() + { + using var trie = LpmTrieIPv4.CreateDefault(); + + bool deleted = trie.Delete("192.168.0.0/16"); + + Assert.False(deleted); + } + + [Fact] + public void LongestPrefixMatch_SelectsMostSpecific() + { + using var trie = LpmTrieIPv4.CreateDefault(); + + trie.Add("0.0.0.0/0", 1); // Default route + trie.Add("192.168.0.0/16", 2); // /16 + trie.Add("192.168.1.0/24", 3); // /24 + trie.Add("192.168.1.128/25", 4); // /25 + + Assert.Equal(4u, trie.Lookup("192.168.1.200")!.Value); // Matches /25 + Assert.Equal(3u, trie.Lookup("192.168.1.100")!.Value); // Matches /24 + Assert.Equal(2u, trie.Lookup("192.168.2.1")!.Value); // Matches /16 + Assert.Equal(1u, trie.Lookup("10.0.0.1")!.Value); // Matches default + } + + [Fact] + public void DefaultRoute_MatchesEverything() + { + using var trie = LpmTrieIPv4.CreateDefault(); + + trie.Add("0.0.0.0/0", 999); + + Assert.Equal(999u, trie.Lookup("1.2.3.4")!.Value); + Assert.Equal(999u, trie.Lookup("255.255.255.255")!.Value); + } + + [Fact] + public void HostRoute_ExactMatch() + { + using var trie = LpmTrieIPv4.CreateDefault(); + + trie.Add("192.168.1.1/32", 100); + + Assert.Equal(100u, trie.Lookup("192.168.1.1")!.Value); + Assert.Null(trie.Lookup("192.168.1.2")); + } + + [Fact] + public void TryAdd_ValidPrefix_ReturnsTrue() + { + using var trie = LpmTrieIPv4.CreateDefault(); + + byte[] prefix = { 192, 168, 0, 0 }; + bool result = trie.TryAdd(prefix, 16, 100); + + Assert.True(result); + } + + [Fact] + public void TryAdd_InvalidPrefixLength_ReturnsFalse() + { + using var trie = LpmTrieIPv4.CreateDefault(); + + byte[] prefix = { 192, 168, 0, 0 }; + bool result = trie.TryAdd(prefix, 33, 100); // Invalid: > 32 + + Assert.False(result); + } + + [Fact] + public void Add_InvalidPrefixLength_ThrowsException() + { + using var trie = LpmTrieIPv4.CreateDefault(); + + byte[] prefix = { 192, 168, 0, 0 }; + + Assert.Throws(() => trie.Add(prefix, 33, 100)); + } + + [Fact] + public void Add_WrongAddressSize_ThrowsException() + { + using var trie = LpmTrieIPv4.CreateDefault(); + + byte[] prefix = { 192, 168, 0 }; // Only 3 bytes + + Assert.Throws(() => trie.Add(prefix, 16, 100)); + } + + [Fact] + public void Add_IPv6Address_ThrowsException() + { + using var trie = LpmTrieIPv4.CreateDefault(); + + Assert.Throws(() => trie.Add("2001:db8::/32", 100)); + } + + [Fact] + public void Lookup_IPv6Address_ThrowsException() + { + using var trie = LpmTrieIPv4.CreateDefault(); + + var addr = IPAddress.Parse("2001:db8::1"); + + Assert.Throws(() => trie.Lookup(addr)); + } + + [Fact] + public void ParseIPv4Address_ValidAddress_Success() + { + uint result = LpmTrieIPv4.ParseIPv4Address("192.168.1.1"); + + // 192.168.1.1 in network byte order = 0xC0A80101 + Assert.Equal(0xC0A80101u, result); + } + + [Fact] + public void ParseIPv4Address_InvalidAddress_ThrowsException() + { + Assert.Throws(() => LpmTrieIPv4.ParseIPv4Address("invalid")); + } + + [Fact] + public void BytesToUInt32_ValidBytes_Success() + { + byte[] bytes = { 192, 168, 1, 1 }; + uint result = LpmTrieIPv4.BytesToUInt32(bytes); + + Assert.Equal(0xC0A80101u, result); + } + + [Fact] + public void UInt32ToBytes_ValidAddress_Success() + { + byte[] result = LpmTrieIPv4.UInt32ToBytes(0xC0A80101); + + Assert.Equal(new byte[] { 192, 168, 1, 1 }, result); + } + + [Fact] + public void Span_AddAndLookup_Success() + { + using var trie = LpmTrieIPv4.CreateDefault(); + + ReadOnlySpan prefix = stackalloc byte[] { 192, 168, 0, 0 }; + trie.Add(prefix, 16, 100); + + ReadOnlySpan addr = stackalloc byte[] { 192, 168, 1, 1 }; + var result = trie.Lookup(addr); + + Assert.Equal(100u, result!.Value); + } + + [Fact] + public void OverwritePrefix_Success() + { + using var trie = LpmTrieIPv4.CreateDefault(); + + trie.Add("192.168.0.0/16", 100); + trie.Add("192.168.0.0/16", 200); // Overwrite + + Assert.Equal(200u, trie.Lookup("192.168.1.1")!.Value); + } + } +} diff --git a/bindings/csharp/LibLpm.Tests/IPv6Tests.cs b/bindings/csharp/LibLpm.Tests/IPv6Tests.cs new file mode 100644 index 0000000..4a7b610 --- /dev/null +++ b/bindings/csharp/LibLpm.Tests/IPv6Tests.cs @@ -0,0 +1,287 @@ +// IPv6Tests.cs - Unit tests for IPv6 LPM operations + +using System; +using System.Net; +using Xunit; + +namespace LibLpm.Tests +{ + /// + /// Tests for IPv6 LPM trie operations. + /// + public class IPv6Tests + { + [Fact] + public void CreateDefault_ReturnsValidTrie() + { + using var trie = LpmTrieIPv6.CreateDefault(); + Assert.NotNull(trie); + Assert.True(trie.IsIPv6); + Assert.False(trie.IsDisposed); + } + + [Fact] + public void CreateWide16_ReturnsValidTrie() + { + using var trie = LpmTrieIPv6.CreateWide16(); + Assert.NotNull(trie); + Assert.Equal(LpmAlgorithm.Wide16, trie.Algorithm); + } + + [Fact] + public void CreateStride8_ReturnsValidTrie() + { + using var trie = LpmTrieIPv6.CreateStride8(); + Assert.NotNull(trie); + Assert.Equal(LpmAlgorithm.Stride8, trie.Algorithm); + } + + [Fact] + public void Create_WithDir24_ThrowsArgumentException() + { + Assert.Throws(() => LpmTrieIPv6.Create(LpmAlgorithm.Dir24)); + } + + [Fact] + public void AddAndLookup_ByteArray_Success() + { + using var trie = LpmTrieIPv6.CreateDefault(); + + // 2001:db8:: + byte[] prefix = new byte[] { 0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + trie.Add(prefix, 32, 100); + + // 2001:db8::1 + byte[] addr = new byte[] { 0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 }; + var result = trie.Lookup(addr); + + Assert.NotNull(result); + Assert.Equal(100u, result.Value); + } + + [Fact] + public void AddAndLookup_String_Success() + { + using var trie = LpmTrieIPv6.CreateDefault(); + + trie.Add("2001:db8::/32", 200); + + var result = trie.Lookup("2001:db8::ffff"); + + Assert.NotNull(result); + Assert.Equal(200u, result.Value); + } + + [Fact] + public void AddAndLookup_IPAddress_Success() + { + using var trie = LpmTrieIPv6.CreateDefault(); + + trie.Add("fc00::/7", 300); + + var addr = IPAddress.Parse("fd00::1"); + var result = trie.Lookup(addr); + + Assert.NotNull(result); + Assert.Equal(300u, result.Value); + } + + [Fact] + public void Lookup_NoMatch_ReturnsNull() + { + using var trie = LpmTrieIPv6.CreateDefault(); + + trie.Add("2001:db8::/32", 100); + + var result = trie.Lookup("2001:db9::1"); + + Assert.Null(result); + } + + [Fact] + public void Delete_ExistingPrefix_ReturnsTrue() + { + using var trie = LpmTrieIPv6.CreateDefault(); + + trie.Add("2001:db8::/32", 100); + + bool deleted = trie.Delete("2001:db8::/32"); + + Assert.True(deleted); + Assert.Null(trie.Lookup("2001:db8::1")); + } + + [Fact] + public void Delete_NonExistingPrefix_ReturnsFalse() + { + using var trie = LpmTrieIPv6.CreateDefault(); + + bool deleted = trie.Delete("2001:db8::/32"); + + Assert.False(deleted); + } + + [Fact] + public void LongestPrefixMatch_SelectsMostSpecific() + { + using var trie = LpmTrieIPv6.CreateDefault(); + + trie.Add("::/0", 1); // Default route + trie.Add("2001:db8::/32", 2); // /32 + trie.Add("2001:db8:1::/48", 3); // /48 + trie.Add("2001:db8:1:1::/64", 4); // /64 + + Assert.Equal(4u, trie.Lookup("2001:db8:1:1::1")!.Value); // Matches /64 + Assert.Equal(3u, trie.Lookup("2001:db8:1:2::1")!.Value); // Matches /48 + Assert.Equal(2u, trie.Lookup("2001:db8:2::1")!.Value); // Matches /32 + Assert.Equal(1u, trie.Lookup("2001:db9::1")!.Value); // Matches default + } + + [Fact] + public void DefaultRoute_MatchesEverything() + { + using var trie = LpmTrieIPv6.CreateDefault(); + + trie.Add("::/0", 999); + + Assert.Equal(999u, trie.Lookup("::1")!.Value); + Assert.Equal(999u, trie.Lookup("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")!.Value); + } + + [Fact] + public void HostRoute_ExactMatch() + { + using var trie = LpmTrieIPv6.CreateDefault(); + + trie.Add("2001:db8::1/128", 100); + + Assert.Equal(100u, trie.Lookup("2001:db8::1")!.Value); + Assert.Null(trie.Lookup("2001:db8::2")); + } + + [Fact] + public void Add_InvalidPrefixLength_ThrowsException() + { + using var trie = LpmTrieIPv6.CreateDefault(); + + byte[] prefix = new byte[16]; + + Assert.Throws(() => trie.Add(prefix, 129, 100)); + } + + [Fact] + public void Add_WrongAddressSize_ThrowsException() + { + using var trie = LpmTrieIPv6.CreateDefault(); + + byte[] prefix = new byte[4]; // Only 4 bytes + + Assert.Throws(() => trie.Add(prefix, 32, 100)); + } + + [Fact] + public void Add_IPv4Address_ThrowsException() + { + using var trie = LpmTrieIPv6.CreateDefault(); + + Assert.Throws(() => trie.Add("192.168.0.0/16", 100)); + } + + [Fact] + public void Lookup_IPv4Address_ThrowsException() + { + using var trie = LpmTrieIPv6.CreateDefault(); + + var addr = IPAddress.Parse("192.168.1.1"); + + Assert.Throws(() => trie.Lookup(addr)); + } + + [Fact] + public void ParseIPv6Address_ValidAddress_Success() + { + byte[] result = LpmTrieIPv6.ParseIPv6Address("2001:db8::1"); + + Assert.Equal(16, result.Length); + Assert.Equal(0x20, result[0]); + Assert.Equal(0x01, result[1]); + Assert.Equal(0x0d, result[2]); + Assert.Equal(0xb8, result[3]); + Assert.Equal(0x01, result[15]); + } + + [Fact] + public void ParseIPv6Address_InvalidAddress_ThrowsException() + { + Assert.Throws(() => LpmTrieIPv6.ParseIPv6Address("invalid")); + } + + [Fact] + public void FormatIPv6Address_ValidBytes_Success() + { + byte[] bytes = new byte[] { 0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 }; + string result = LpmTrieIPv6.FormatIPv6Address(bytes); + + Assert.Contains("2001:db8", result.ToLower()); + } + + [Fact] + public void CreateAddressBuffer_Returns16Bytes() + { + byte[] buffer = LpmTrieIPv6.CreateAddressBuffer(); + + Assert.Equal(16, buffer.Length); + Assert.All(buffer, b => Assert.Equal(0, b)); + } + + [Fact] + public void Span_AddAndLookup_Success() + { + using var trie = LpmTrieIPv6.CreateDefault(); + + Span prefix = stackalloc byte[16]; + prefix[0] = 0x20; + prefix[1] = 0x01; + prefix[2] = 0x0d; + prefix[3] = 0xb8; + trie.Add(prefix, 32, 100); + + Span addr = stackalloc byte[16]; + prefix.CopyTo(addr); + addr[15] = 1; + var result = trie.Lookup(addr); + + Assert.Equal(100u, result!.Value); + } + + [Fact] + public void OverwritePrefix_Success() + { + using var trie = LpmTrieIPv6.CreateDefault(); + + trie.Add("2001:db8::/32", 100); + trie.Add("2001:db8::/32", 200); // Overwrite + + Assert.Equal(200u, trie.Lookup("2001:db8::1")!.Value); + } + + [Fact] + public void CommonIPv6Prefixes_Success() + { + using var trie = LpmTrieIPv6.CreateDefault(); + + // Common IPv6 prefixes + trie.Add("::1/128", 1); // Loopback + trie.Add("fe80::/10", 2); // Link-local + trie.Add("fc00::/7", 3); // Unique local + trie.Add("2000::/3", 4); // Global unicast + trie.Add("ff00::/8", 5); // Multicast + + Assert.Equal(1u, trie.Lookup("::1")!.Value); + Assert.Equal(2u, trie.Lookup("fe80::1")!.Value); + Assert.Equal(3u, trie.Lookup("fd00::1")!.Value); + Assert.Equal(4u, trie.Lookup("2001:db8::1")!.Value); + Assert.Equal(5u, trie.Lookup("ff02::1")!.Value); + } + } +} diff --git a/bindings/csharp/LibLpm.Tests/LibLpm.Tests.csproj b/bindings/csharp/LibLpm.Tests/LibLpm.Tests.csproj new file mode 100644 index 0000000..373bbf7 --- /dev/null +++ b/bindings/csharp/LibLpm.Tests/LibLpm.Tests.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + latest + enable + true + false + true + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/bindings/csharp/LibLpm.Tests/ResourceTests.cs b/bindings/csharp/LibLpm.Tests/ResourceTests.cs new file mode 100644 index 0000000..c7e0576 --- /dev/null +++ b/bindings/csharp/LibLpm.Tests/ResourceTests.cs @@ -0,0 +1,240 @@ +// ResourceTests.cs - Unit tests for resource management and lifecycle + +using System; +using Xunit; + +namespace LibLpm.Tests +{ + /// + /// Tests for resource management and object lifecycle. + /// + public class ResourceTests + { + [Fact] + public void Dispose_SetsIsDisposed() + { + var trie = LpmTrieIPv4.CreateDefault(); + Assert.False(trie.IsDisposed); + + trie.Dispose(); + + Assert.True(trie.IsDisposed); + } + + [Fact] + public void Dispose_MultipleCalls_Safe() + { + var trie = LpmTrieIPv4.CreateDefault(); + + trie.Dispose(); + trie.Dispose(); // Should not throw + trie.Dispose(); + + Assert.True(trie.IsDisposed); + } + + [Fact] + public void Add_AfterDispose_ThrowsObjectDisposedException() + { + var trie = LpmTrieIPv4.CreateDefault(); + trie.Dispose(); + + Assert.Throws(() => trie.Add("192.168.0.0/16", 100)); + } + + [Fact] + public void Lookup_AfterDispose_ThrowsObjectDisposedException() + { + var trie = LpmTrieIPv4.CreateDefault(); + trie.Dispose(); + + Assert.Throws(() => trie.Lookup("192.168.1.1")); + } + + [Fact] + public void Delete_AfterDispose_ThrowsObjectDisposedException() + { + var trie = LpmTrieIPv4.CreateDefault(); + trie.Dispose(); + + Assert.Throws(() => trie.Delete("192.168.0.0/16")); + } + + [Fact] + public void NativeHandle_AfterDispose_ThrowsObjectDisposedException() + { + var trie = LpmTrieIPv4.CreateDefault(); + trie.Dispose(); + + Assert.Throws(() => _ = trie.NativeHandle); + } + + [Fact] + public void Using_DisposesAutomatically() + { + LpmTrieIPv4 trie; + + using (trie = LpmTrieIPv4.CreateDefault()) + { + Assert.False(trie.IsDisposed); + } + + Assert.True(trie.IsDisposed); + } + + [Fact] + public void TryAdd_AfterDispose_ReturnsFalse() + { + var trie = LpmTrieIPv4.CreateDefault(); + trie.Dispose(); + + byte[] prefix = { 192, 168, 0, 0 }; + bool result = trie.TryAdd(prefix, 16, 100); + + Assert.False(result); + } + + [Fact] + public void IPv6_Dispose_Works() + { + var trie = LpmTrieIPv6.CreateDefault(); + Assert.False(trie.IsDisposed); + + trie.Dispose(); + + Assert.True(trie.IsDisposed); + } + + [Fact] + public void IPv6_Add_AfterDispose_ThrowsObjectDisposedException() + { + var trie = LpmTrieIPv6.CreateDefault(); + trie.Dispose(); + + Assert.Throws(() => trie.Add("2001:db8::/32", 100)); + } + + [Fact] + public void GetVersion_ReturnsString() + { + var version = LpmTrie.GetVersion(); + + Assert.NotNull(version); + Assert.NotEmpty(version); + } + + [Fact] + public void NativeLibraryLoader_FindLibraryPath_ReturnsPathOrNull() + { + var path = NativeLibraryLoader.FindLibraryPath(); + + // Path may or may not be found depending on environment + // Just ensure no exception is thrown + } + + [Fact] + public void NativeLibraryLoader_RuntimeIdentifier_ReturnsValidRid() + { + var rid = NativeLibraryLoader.RuntimeIdentifier; + + Assert.NotNull(rid); + Assert.NotEmpty(rid); + Assert.Contains("-", rid); // e.g., linux-x64, win-x64 + } + + [Fact] + public void NativeLibraryLoader_NativeLibraryName_ReturnsValidName() + { + var name = NativeLibraryLoader.NativeLibraryName; + + Assert.NotNull(name); + Assert.NotEmpty(name); + Assert.True( + name.EndsWith(".so") || + name.EndsWith(".dll") || + name.EndsWith(".dylib")); + } + + [Fact] + public void SafeLpmHandle_Invalid_IsInvalid() + { + var handle = SafeLpmHandle.Invalid; + + Assert.True(handle.IsInvalid); + } + + [Fact] + public void PrintStats_AfterDispose_ThrowsObjectDisposedException() + { + var trie = LpmTrieIPv4.CreateDefault(); + trie.Dispose(); + + Assert.Throws(() => trie.PrintStats()); + } + + [Fact] + public void MaxPrefixLength_IPv4_Returns32() + { + using var trie = LpmTrieIPv4.CreateDefault(); + Assert.Equal(32, trie.MaxPrefixLength); + } + + [Fact] + public void MaxPrefixLength_IPv6_Returns128() + { + using var trie = LpmTrieIPv6.CreateDefault(); + Assert.Equal(128, trie.MaxPrefixLength); + } + + [Fact] + public void PrefixByteLength_IPv4_Returns4() + { + using var trie = LpmTrieIPv4.CreateDefault(); + Assert.Equal(4, trie.PrefixByteLength); + } + + [Fact] + public void PrefixByteLength_IPv6_Returns16() + { + using var trie = LpmTrieIPv6.CreateDefault(); + Assert.Equal(16, trie.PrefixByteLength); + } + + [Fact] + public void LpmConstants_InvalidNextHop_IsMaxUInt() + { + Assert.Equal(uint.MaxValue, LpmConstants.InvalidNextHop); + } + + [Fact] + public void LpmConstants_IPv4MaxDepth_Is32() + { + Assert.Equal(32, LpmConstants.IPv4MaxDepth); + } + + [Fact] + public void LpmConstants_IPv6MaxDepth_Is128() + { + Assert.Equal(128, LpmConstants.IPv6MaxDepth); + } + + [Fact] + public void MultipleTriesCanCoexist() + { + using var trie1 = LpmTrieIPv4.CreateDefault(); + using var trie2 = LpmTrieIPv4.CreateDir24(); + using var trie3 = LpmTrieIPv6.CreateDefault(); + + trie1.Add("192.168.0.0/16", 1); + trie2.Add("10.0.0.0/8", 2); + trie3.Add("2001:db8::/32", 3); + + Assert.Equal(1u, trie1.Lookup("192.168.1.1")!.Value); + Assert.Equal(2u, trie2.Lookup("10.1.1.1")!.Value); + Assert.Equal(3u, trie3.Lookup("2001:db8::1")!.Value); + + // Cross-check: trie1 shouldn't have trie2's data + Assert.Null(trie1.Lookup("10.1.1.1")); + } + } +} diff --git a/bindings/csharp/LibLpm.sln b/bindings/csharp/LibLpm.sln new file mode 100644 index 0000000..94d7cf2 --- /dev/null +++ b/bindings/csharp/LibLpm.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibLpm", "LibLpm\LibLpm.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibLpm.Tests", "LibLpm.Tests\LibLpm.Tests.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F23456789012}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibLpm.Examples", "LibLpm.Examples\LibLpm.Examples.csproj", "{C3D4E5F6-A7B8-9012-CDEF-345678901234}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F23456789012}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F23456789012}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F23456789012}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F23456789012}.Release|Any CPU.Build.0 = Release|Any CPU + {C3D4E5F6-A7B8-9012-CDEF-345678901234}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C3D4E5F6-A7B8-9012-CDEF-345678901234}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C3D4E5F6-A7B8-9012-CDEF-345678901234}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C3D4E5F6-A7B8-9012-CDEF-345678901234}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/bindings/csharp/LibLpm/LibLpm.csproj b/bindings/csharp/LibLpm/LibLpm.csproj new file mode 100644 index 0000000..6d49080 --- /dev/null +++ b/bindings/csharp/LibLpm/LibLpm.csproj @@ -0,0 +1,99 @@ + + + + netstandard2.1 + latest + enable + true + true + false + + + LibLpm + LibLpm + 2.0.0 + 2.0.0.0 + 2.0.0.0 + + + liblpm + Murilo Chianfa + liblpm + liblpm + Copyright (c) 2024 Murilo Chianfa + High-performance Longest Prefix Match (LPM) library for IP routing. Provides efficient IPv4 and IPv6 route lookups using optimized trie data structures with SIMD acceleration. + BSD-2-Clause + https://github.com/MuriloChianfa/liblpm + https://github.com/MuriloChianfa/liblpm + git + lpm;routing;ipv4;ipv6;networking;trie;longest-prefix-match;cidr;ip-routing;network;high-performance + README.md + Initial release of C# bindings for liblpm 2.0.0 + + + + + + true + true + snupkg + + + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bindings/csharp/LibLpm/LpmAlgorithm.cs b/bindings/csharp/LibLpm/LpmAlgorithm.cs new file mode 100644 index 0000000..6b5f66c --- /dev/null +++ b/bindings/csharp/LibLpm/LpmAlgorithm.cs @@ -0,0 +1,47 @@ +// LpmAlgorithm.cs - Algorithm selection enum +// Defines the available LPM algorithms + +namespace LibLpm +{ + /// + /// Specifies the algorithm to use for the LPM trie. + /// + public enum LpmAlgorithm + { + /// + /// Use the compile-time default algorithm. + /// For IPv4: DIR-24-8 + /// For IPv6: Wide 16-bit stride + /// + Default = 0, + + /// + /// IPv4 DIR-24-8 algorithm. + /// Uses a 24-bit direct table with 8-bit extension tables. + /// Optimal for IPv4 with 1-2 memory accesses per lookup. + /// Memory usage: ~64MB base + extensions for /25-/32 routes. + /// + /// + /// Only valid for IPv4 tries. + /// + Dir24 = 1, + + /// + /// Wide 16-bit stride algorithm for IPv6. + /// Uses 16-bit stride for the first level, then 8-bit strides. + /// Optimal for IPv6 with common /48 allocations. + /// + /// + /// Only valid for IPv6 tries. + /// + Wide16 = 2, + + /// + /// Standard 8-bit stride algorithm. + /// Works for both IPv4 (4 levels max) and IPv6 (16 levels max). + /// Good balance of memory efficiency and lookup speed. + /// Memory-efficient for sparse prefix sets. + /// + Stride8 = 3 + } +} diff --git a/bindings/csharp/LibLpm/LpmException.cs b/bindings/csharp/LibLpm/LpmException.cs new file mode 100644 index 0000000..f14a0f2 --- /dev/null +++ b/bindings/csharp/LibLpm/LpmException.cs @@ -0,0 +1,272 @@ +// LpmException.cs - Exception hierarchy for liblpm +// Provides meaningful exception types for various error conditions + +using System; + +namespace LibLpm +{ + /// + /// Base exception class for all liblpm-related errors. + /// + public class LpmException : Exception + { + /// + /// Initializes a new instance of the class. + /// + public LpmException() + : base("An error occurred in the LPM library.") + { + } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public LpmException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class with a specified error message + /// and a reference to the inner exception that is the cause of this exception. + /// + /// The message that describes the error. + /// The exception that is the cause of the current exception. + public LpmException(string message, Exception innerException) + : base(message, innerException) + { + } + } + + /// + /// Exception thrown when creating an LPM trie fails. + /// This typically indicates a memory allocation failure. + /// + public class LpmCreationException : LpmException + { + /// + /// Gets the algorithm that was requested when the creation failed. + /// + public LpmAlgorithm? Algorithm { get; } + + /// + /// Gets whether the trie was being created for IPv6. + /// + public bool IsIPv6 { get; } + + /// + /// Initializes a new instance of the class. + /// + public LpmCreationException() + : base("Failed to create LPM trie. The native library may have failed to allocate memory.") + { + } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public LpmCreationException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class with details about the creation attempt. + /// + /// Whether the trie was being created for IPv6. + /// The algorithm that was requested. + public LpmCreationException(bool isIPv6, LpmAlgorithm? algorithm = null) + : base($"Failed to create {(isIPv6 ? "IPv6" : "IPv4")} LPM trie" + + (algorithm.HasValue ? $" with algorithm {algorithm.Value}" : "") + + ". The native library may have failed to allocate memory.") + { + IsIPv6 = isIPv6; + Algorithm = algorithm; + } + + /// + /// Initializes a new instance of the class with a specified error message + /// and a reference to the inner exception that is the cause of this exception. + /// + /// The message that describes the error. + /// The exception that is the cause of the current exception. + public LpmCreationException(string message, Exception innerException) + : base(message, innerException) + { + } + } + + /// + /// Exception thrown when an invalid prefix is provided to an LPM operation. + /// + public class LpmInvalidPrefixException : LpmException + { + /// + /// Gets the prefix string that was invalid (if available). + /// + public string? PrefixString { get; } + + /// + /// Gets the prefix length that was invalid (if available). + /// + public byte? PrefixLength { get; } + + /// + /// Gets the maximum allowed prefix length for the address type. + /// + public byte? MaxPrefixLength { get; } + + /// + /// Initializes a new instance of the class. + /// + public LpmInvalidPrefixException() + : base("Invalid prefix format or length.") + { + } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public LpmInvalidPrefixException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class for an invalid prefix string. + /// + /// The invalid prefix string. + /// Additional reason for the failure (optional). + public LpmInvalidPrefixException(string prefixString, string? reason = null) + : base($"Invalid prefix format: '{prefixString}'" + (reason != null ? $". {reason}" : "")) + { + PrefixString = prefixString; + } + + /// + /// Initializes a new instance of the class for an invalid prefix length. + /// + /// The invalid prefix length. + /// The maximum allowed prefix length. + public LpmInvalidPrefixException(byte prefixLength, byte maxPrefixLength) + : base($"Invalid prefix length: {prefixLength}. Maximum allowed is {maxPrefixLength}.") + { + PrefixLength = prefixLength; + MaxPrefixLength = maxPrefixLength; + } + + /// + /// Initializes a new instance of the class with a specified error message + /// and a reference to the inner exception that is the cause of this exception. + /// + /// The message that describes the error. + /// The exception that is the cause of the current exception. + public LpmInvalidPrefixException(string message, Exception innerException) + : base(message, innerException) + { + } + } + + /// + /// Exception thrown when an LPM operation (add, delete) fails. + /// + public class LpmOperationException : LpmException + { + /// + /// Gets the operation that failed. + /// + public string? Operation { get; } + + /// + /// Initializes a new instance of the class. + /// + public LpmOperationException() + : base("An LPM operation failed.") + { + } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public LpmOperationException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class for a specific operation. + /// + /// The operation that failed (e.g., "add", "delete"). + /// Additional details about the failure (optional). + public LpmOperationException(string operation, string? details) + : base($"LPM {operation} operation failed" + (details != null ? $": {details}" : ".")) + { + Operation = operation; + } + + /// + /// Initializes a new instance of the class with a specified error message + /// and a reference to the inner exception that is the cause of this exception. + /// + /// The message that describes the error. + /// The exception that is the cause of the current exception. + public LpmOperationException(string message, Exception innerException) + : base(message, innerException) + { + } + } + + /// + /// Exception thrown when the native library cannot be loaded. + /// + public class LpmLibraryNotFoundException : LpmException + { + /// + /// Gets the paths that were searched for the library. + /// + public string[]? SearchedPaths { get; } + + /// + /// Initializes a new instance of the class. + /// + public LpmLibraryNotFoundException() + : base("The native liblpm library could not be found. Ensure it is installed or included in the runtimes directory.") + { + } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public LpmLibraryNotFoundException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class with searched paths. + /// + /// The paths that were searched for the library. + public LpmLibraryNotFoundException(string[] searchedPaths) + : base("The native liblpm library could not be found. Searched paths: " + + string.Join(", ", searchedPaths)) + { + SearchedPaths = searchedPaths; + } + + /// + /// Initializes a new instance of the class with a specified error message + /// and a reference to the inner exception that is the cause of this exception. + /// + /// The message that describes the error. + /// The exception that is the cause of the current exception. + public LpmLibraryNotFoundException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} diff --git a/bindings/csharp/LibLpm/LpmTrie.cs b/bindings/csharp/LibLpm/LpmTrie.cs new file mode 100644 index 0000000..773cbec --- /dev/null +++ b/bindings/csharp/LibLpm/LpmTrie.cs @@ -0,0 +1,438 @@ +// LpmTrie.cs - Base wrapper class for LPM trie +// Provides common functionality for IPv4 and IPv6 tries + +using System; +using System.Net; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; + +namespace LibLpm +{ + /// + /// Base class for LPM (Longest Prefix Match) trie. + /// Provides common functionality for IPv4 and IPv6 tries. + /// + /// + /// This class is not thread-safe. For concurrent access, use external synchronization. + /// Implements IDisposable for deterministic resource cleanup. + /// + public abstract class LpmTrie : IDisposable + { + /// + /// The safe handle to the native trie. + /// + protected SafeLpmHandle _handle; + + /// + /// Whether the trie has been disposed. + /// + private bool _disposed = false; + + /// + /// Static constructor to initialize native library resolver. + /// + static LpmTrie() + { + NativeLibraryLoader.Initialize(); + } + + /// + /// Initializes a new instance of the class. + /// + /// The native handle to wrap. + protected LpmTrie(SafeLpmHandle handle) + { + _handle = handle ?? throw new ArgumentNullException(nameof(handle)); + if (_handle.IsInvalid) + { + throw new LpmCreationException("Invalid handle provided."); + } + } + + /// + /// Gets whether this is an IPv6 trie. + /// + public abstract bool IsIPv6 { get; } + + /// + /// Gets the maximum prefix length for this trie type. + /// + public byte MaxPrefixLength => IsIPv6 ? LpmConstants.IPv6MaxDepth : LpmConstants.IPv4MaxDepth; + + /// + /// Gets the expected prefix byte length for this trie type. + /// + public int PrefixByteLength => IsIPv6 ? 16 : 4; + + /// + /// Gets whether the trie has been disposed. + /// + public bool IsDisposed => _disposed; + + /// + /// Gets the native handle. For advanced users only. + /// + public IntPtr NativeHandle + { + get + { + ThrowIfDisposed(); + return _handle.DangerousGetHandle(); + } + } + + /// + /// Adds a prefix to the trie with the specified next hop. + /// + /// The prefix bytes (4 for IPv4, 16 for IPv6). + /// The prefix length in bits. + /// The next hop value to associate with the prefix. + /// Thrown if prefix is null. + /// Thrown if prefix length is invalid. + /// Thrown if the add operation fails. + /// Thrown if the trie has been disposed. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(byte[] prefix, byte prefixLen, uint nextHop) + { + ThrowIfDisposed(); + ValidatePrefix(prefix, prefixLen); + + int result = NativeMethods.lpm_add(_handle.DangerousGetHandle(), prefix, prefixLen, nextHop); + if (result != 0) + { + throw new LpmOperationException("add", "Failed to add prefix to trie."); + } + } + + /// + /// Adds a prefix to the trie with the specified next hop (Span version for zero-copy). + /// + /// The prefix bytes (4 for IPv4, 16 for IPv6). + /// The prefix length in bits. + /// The next hop value to associate with the prefix. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void Add(ReadOnlySpan prefix, byte prefixLen, uint nextHop) + { + ThrowIfDisposed(); + if (prefix.Length != PrefixByteLength) + { + throw new LpmInvalidPrefixException($"Prefix must be {PrefixByteLength} bytes for {(IsIPv6 ? "IPv6" : "IPv4")}."); + } + if (prefixLen > MaxPrefixLength) + { + throw new LpmInvalidPrefixException(prefixLen, MaxPrefixLength); + } + + fixed (byte* ptr = prefix) + { + int result = NativeMethods.lpm_add(_handle.DangerousGetHandle(), ptr, prefixLen, nextHop); + if (result != 0) + { + throw new LpmOperationException("add", "Failed to add prefix to trie."); + } + } + } + + /// + /// Adds a prefix to the trie using CIDR notation (e.g., "192.168.0.0/16"). + /// + /// The prefix in CIDR notation. + /// The next hop value to associate with the prefix. + /// Thrown if cidr is null. + /// Thrown if the CIDR notation is invalid. + /// Thrown if the add operation fails. + /// Thrown if the trie has been disposed. + public void Add(string cidr, uint nextHop) + { + if (string.IsNullOrEmpty(cidr)) + { + throw new ArgumentNullException(nameof(cidr)); + } + + var (prefix, prefixLen) = ParseCidr(cidr); + Add(prefix, prefixLen, nextHop); + } + + /// + /// Tries to add a prefix to the trie. Returns false if the operation fails. + /// + /// The prefix bytes. + /// The prefix length in bits. + /// The next hop value. + /// True if successful, false otherwise. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryAdd(byte[] prefix, byte prefixLen, uint nextHop) + { + if (_disposed || prefix == null || prefix.Length != PrefixByteLength || prefixLen > MaxPrefixLength) + { + return false; + } + + return NativeMethods.lpm_add(_handle.DangerousGetHandle(), prefix, prefixLen, nextHop) == 0; + } + + /// + /// Deletes a prefix from the trie. + /// + /// The prefix bytes (4 for IPv4, 16 for IPv6). + /// The prefix length in bits. + /// True if the prefix was deleted, false if it was not found. + /// Thrown if prefix is null. + /// Thrown if prefix length is invalid. + /// Thrown if the trie has been disposed. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Delete(byte[] prefix, byte prefixLen) + { + ThrowIfDisposed(); + ValidatePrefix(prefix, prefixLen); + + return NativeMethods.lpm_delete(_handle.DangerousGetHandle(), prefix, prefixLen) == 0; + } + + /// + /// Deletes a prefix from the trie (Span version for zero-copy). + /// + /// The prefix bytes. + /// The prefix length in bits. + /// True if the prefix was deleted, false if it was not found. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe bool Delete(ReadOnlySpan prefix, byte prefixLen) + { + ThrowIfDisposed(); + if (prefix.Length != PrefixByteLength) + { + throw new LpmInvalidPrefixException($"Prefix must be {PrefixByteLength} bytes for {(IsIPv6 ? "IPv6" : "IPv4")}."); + } + if (prefixLen > MaxPrefixLength) + { + throw new LpmInvalidPrefixException(prefixLen, MaxPrefixLength); + } + + fixed (byte* ptr = prefix) + { + return NativeMethods.lpm_delete(_handle.DangerousGetHandle(), ptr, prefixLen) == 0; + } + } + + /// + /// Deletes a prefix from the trie using CIDR notation. + /// + /// The prefix in CIDR notation. + /// True if the prefix was deleted, false if it was not found. + public bool Delete(string cidr) + { + if (string.IsNullOrEmpty(cidr)) + { + throw new ArgumentNullException(nameof(cidr)); + } + + var (prefix, prefixLen) = ParseCidr(cidr); + return Delete(prefix, prefixLen); + } + + /// + /// Performs a lookup in the trie for the given address. + /// + /// The address bytes (4 for IPv4, 16 for IPv6). + /// The next hop value if found, or null if no match. + /// Thrown if address is null. + /// Thrown if address length is invalid. + /// Thrown if the trie has been disposed. + public abstract uint? Lookup(byte[] address); + + /// + /// Performs a lookup in the trie for the given address (Span version). + /// + /// The address bytes. + /// The next hop value if found, or null if no match. + public abstract uint? Lookup(ReadOnlySpan address); + + /// + /// Performs a lookup in the trie for the given IP address. + /// + /// The IP address. + /// The next hop value if found, or null if no match. + public uint? Lookup(IPAddress address) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + + var bytes = address.GetAddressBytes(); + if (bytes.Length != PrefixByteLength) + { + throw new ArgumentException( + $"Address must be {(IsIPv6 ? "IPv6" : "IPv4")} for this trie.", + nameof(address)); + } + + return Lookup(bytes); + } + + /// + /// Performs a lookup in the trie for the given address string. + /// + /// The IP address as a string. + /// The next hop value if found, or null if no match. + public uint? Lookup(string address) + { + if (string.IsNullOrEmpty(address)) + { + throw new ArgumentNullException(nameof(address)); + } + + if (!IPAddress.TryParse(address, out var ipAddress)) + { + throw new ArgumentException($"Invalid IP address: {address}", nameof(address)); + } + + return Lookup(ipAddress); + } + + /// + /// Prints trie statistics to stdout. + /// + public void PrintStats() + { + ThrowIfDisposed(); + NativeMethods.lpm_print_stats(_handle.DangerousGetHandle()); + } + + /// + /// Gets the library version string. + /// + /// The version string. + public static string? GetVersion() + { + NativeLibraryLoader.Initialize(); + return NativeLibraryLoader.GetVersion(); + } + + /// + /// Validates the prefix and prefix length. + /// + protected void ValidatePrefix(byte[]? prefix, byte prefixLen) + { + if (prefix == null) + { + throw new ArgumentNullException(nameof(prefix)); + } + if (prefix.Length != PrefixByteLength) + { + throw new LpmInvalidPrefixException( + $"Prefix must be {PrefixByteLength} bytes for {(IsIPv6 ? "IPv6" : "IPv4")}."); + } + if (prefixLen > MaxPrefixLength) + { + throw new LpmInvalidPrefixException(prefixLen, MaxPrefixLength); + } + } + + /// + /// Validates the address length. + /// + protected void ValidateAddress(byte[]? address) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + if (address.Length != PrefixByteLength) + { + throw new ArgumentException( + $"Address must be {PrefixByteLength} bytes for {(IsIPv6 ? "IPv6" : "IPv4")}.", + nameof(address)); + } + } + + /// + /// Parses a CIDR notation string into prefix bytes and length. + /// + protected (byte[] prefix, byte prefixLen) ParseCidr(string cidr) + { + var parts = cidr.Split('/'); + if (parts.Length != 2) + { + throw new LpmInvalidPrefixException(cidr, "Expected format: address/prefix_length"); + } + + if (!IPAddress.TryParse(parts[0], out var address)) + { + throw new LpmInvalidPrefixException(cidr, "Invalid IP address"); + } + + if (!byte.TryParse(parts[1], out var prefixLen)) + { + throw new LpmInvalidPrefixException(cidr, "Invalid prefix length"); + } + + var bytes = address.GetAddressBytes(); + + // Validate address family matches trie type + bool isIPv6Address = address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6; + if (isIPv6Address != IsIPv6) + { + throw new LpmInvalidPrefixException(cidr, + $"Address family mismatch: expected {(IsIPv6 ? "IPv6" : "IPv4")}"); + } + + if (prefixLen > MaxPrefixLength) + { + throw new LpmInvalidPrefixException(prefixLen, MaxPrefixLength); + } + + return (bytes, prefixLen); + } + + /// + /// Throws ObjectDisposedException if the trie has been disposed. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected void ThrowIfDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(GetType().Name); + } + } + + /// + /// Disposes the trie and releases all native resources. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Disposes the trie. + /// + /// True if called from Dispose(), false if from finalizer. + protected virtual void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + if (disposing) + { + // Dispose managed resources + _handle?.Dispose(); + } + + _disposed = true; + } + + /// + /// Finalizer to ensure resources are released. + /// + ~LpmTrie() + { + Dispose(false); + } + } +} diff --git a/bindings/csharp/LibLpm/LpmTrieIPv4.cs b/bindings/csharp/LibLpm/LpmTrieIPv4.cs new file mode 100644 index 0000000..5cd4d94 --- /dev/null +++ b/bindings/csharp/LibLpm/LpmTrieIPv4.cs @@ -0,0 +1,328 @@ +// LpmTrieIPv4.cs - IPv4-specific LPM trie wrapper +// Provides optimized IPv4 operations and convenience methods + +using System; +using System.Buffers.Binary; +using System.Net; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace LibLpm +{ + /// + /// IPv4-specific LPM (Longest Prefix Match) trie. + /// Provides optimized operations for IPv4 address lookups. + /// + /// + /// This class is not thread-safe. For concurrent access, use external synchronization. + /// Implements IDisposable for deterministic resource cleanup. + /// + public sealed class LpmTrieIPv4 : LpmTrie + { + private readonly LpmAlgorithm _algorithm; + + /// + /// Initializes a new instance of the class. + /// + /// The native handle to wrap. + /// The algorithm used for this trie. + private LpmTrieIPv4(SafeLpmHandle handle, LpmAlgorithm algorithm) : base(handle) + { + _algorithm = algorithm; + } + + /// + public override bool IsIPv6 => false; + + /// + /// Gets the algorithm used for this trie. + /// + public LpmAlgorithm Algorithm => _algorithm; + + // ============================================================================ + // Factory Methods + // ============================================================================ + + /// + /// Creates a new IPv4 LPM trie using the default algorithm (DIR-24-8). + /// + /// A new IPv4 LPM trie. + /// Thrown if the trie creation fails. + public static LpmTrieIPv4 CreateDefault() + { + NativeLibraryLoader.Initialize(); + var ptr = NativeMethods.lpm_create_ipv4(); + if (ptr == IntPtr.Zero) + { + throw new LpmCreationException(isIPv6: false, LpmAlgorithm.Default); + } + return new LpmTrieIPv4(new SafeLpmHandle(ptr), LpmAlgorithm.Default); + } + + /// + /// Creates a new IPv4 LPM trie using DIR-24-8 algorithm explicitly. + /// + /// A new IPv4 LPM trie with DIR-24-8 algorithm. + /// Thrown if the trie creation fails. + public static LpmTrieIPv4 CreateDir24() + { + NativeLibraryLoader.Initialize(); + var ptr = NativeMethods.lpm_create_ipv4_dir24(); + if (ptr == IntPtr.Zero) + { + throw new LpmCreationException(isIPv6: false, LpmAlgorithm.Dir24); + } + return new LpmTrieIPv4(new SafeLpmHandle(ptr), LpmAlgorithm.Dir24); + } + + /// + /// Creates a new IPv4 LPM trie using 8-bit stride algorithm. + /// + /// A new IPv4 LPM trie with 8-bit stride algorithm. + /// Thrown if the trie creation fails. + public static LpmTrieIPv4 CreateStride8() + { + NativeLibraryLoader.Initialize(); + var ptr = NativeMethods.lpm_create_ipv4_8stride(); + if (ptr == IntPtr.Zero) + { + throw new LpmCreationException(isIPv6: false, LpmAlgorithm.Stride8); + } + return new LpmTrieIPv4(new SafeLpmHandle(ptr), LpmAlgorithm.Stride8); + } + + /// + /// Creates a new IPv4 LPM trie with the specified algorithm. + /// + /// The algorithm to use. + /// A new IPv4 LPM trie. + /// Thrown if the trie creation fails. + /// Thrown if an IPv6-only algorithm is specified. + public static LpmTrieIPv4 Create(LpmAlgorithm algorithm) + { + return algorithm switch + { + LpmAlgorithm.Default => CreateDefault(), + LpmAlgorithm.Dir24 => CreateDir24(), + LpmAlgorithm.Stride8 => CreateStride8(), + LpmAlgorithm.Wide16 => throw new ArgumentException( + "Wide16 algorithm is only valid for IPv6 tries.", nameof(algorithm)), + _ => throw new ArgumentOutOfRangeException(nameof(algorithm)) + }; + } + + // ============================================================================ + // Lookup Operations + // ============================================================================ + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override uint? Lookup(byte[] address) + { + ThrowIfDisposed(); + ValidateAddress(address); + + // Convert bytes to uint32 in network byte order (big-endian) + uint addr32 = BytesToUInt32(address); + uint result = NativeMethods.lpm_lookup_ipv4(_handle.DangerousGetHandle(), addr32); + return result == LpmConstants.InvalidNextHop ? null : result; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override unsafe uint? Lookup(ReadOnlySpan address) + { + ThrowIfDisposed(); + if (address.Length != 4) + { + throw new ArgumentException("IPv4 address must be 4 bytes.", nameof(address)); + } + + // Convert bytes to uint32 in network byte order (big-endian) + uint addr32 = (uint)(address[0] << 24 | address[1] << 16 | address[2] << 8 | address[3]); + uint result = NativeMethods.lpm_lookup_ipv4(_handle.DangerousGetHandle(), addr32); + return result == LpmConstants.InvalidNextHop ? null : result; + } + + /// + /// Performs a lookup for the given IPv4 address as a 32-bit integer. + /// + /// The IPv4 address in network byte order (big-endian). + /// The next hop value if found, or null if no match. + /// Thrown if the trie has been disposed. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public uint? Lookup(uint address) + { + ThrowIfDisposed(); + uint result = NativeMethods.lpm_lookup_ipv4(_handle.DangerousGetHandle(), address); + return result == LpmConstants.InvalidNextHop ? null : result; + } + + /// + /// Performs a raw lookup returning the raw next hop value (including InvalidNextHop). + /// This is the fastest lookup method with no null checking overhead. + /// + /// The IPv4 address in network byte order. + /// The raw next hop value, or LpmConstants.InvalidNextHop if no match. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public uint LookupRaw(uint address) + { + ThrowIfDisposed(); + return NativeMethods.lpm_lookup_ipv4(_handle.DangerousGetHandle(), address); + } + + // ============================================================================ + // Batch Operations + // ============================================================================ + + /// + /// Performs batch lookups for multiple IPv4 addresses. + /// + /// Array of IPv4 addresses (network byte order). + /// Output array for next hop values. + /// Thrown if addresses or nextHops is null. + /// Thrown if array lengths don't match. + /// Thrown if the trie has been disposed. + public void LookupBatch(uint[] addresses, uint[] nextHops) + { + ThrowIfDisposed(); + if (addresses == null) throw new ArgumentNullException(nameof(addresses)); + if (nextHops == null) throw new ArgumentNullException(nameof(nextHops)); + if (addresses.Length > nextHops.Length) + { + throw new ArgumentException("Output array must be at least as large as input array."); + } + + NativeMethods.lpm_lookup_batch_ipv4( + _handle.DangerousGetHandle(), + addresses, + nextHops, + (UIntPtr)addresses.Length); + } + + /// + /// Performs batch lookups for multiple IPv4 addresses (Span version for zero-copy). + /// + /// Span of IPv4 addresses (network byte order). + /// Output span for next hop values. + /// Thrown if output span is too small. + /// Thrown if the trie has been disposed. + public unsafe void LookupBatch(ReadOnlySpan addresses, Span nextHops) + { + ThrowIfDisposed(); + if (addresses.Length > nextHops.Length) + { + throw new ArgumentException("Output span must be at least as large as input span."); + } + + fixed (uint* addrPtr = addresses) + fixed (uint* nhPtr = nextHops) + { + NativeMethods.lpm_lookup_batch_ipv4( + _handle.DangerousGetHandle(), + addrPtr, + nhPtr, + (UIntPtr)addresses.Length); + } + } + + /// + /// Performs batch lookups for multiple IPv4 addresses as byte arrays. + /// + /// Array of IPv4 addresses (each 4 bytes, contiguous). + /// Output array for next hop values. + /// Thrown if array sizes are invalid. + /// Thrown if the trie has been disposed. + public void LookupBatch(byte[] addresses, uint[] nextHops) + { + ThrowIfDisposed(); + if (addresses == null) throw new ArgumentNullException(nameof(addresses)); + if (nextHops == null) throw new ArgumentNullException(nameof(nextHops)); + if (addresses.Length % 4 != 0) + { + throw new ArgumentException("Addresses array length must be a multiple of 4.", nameof(addresses)); + } + + int count = addresses.Length / 4; + if (count > nextHops.Length) + { + throw new ArgumentException("Output array must be at least as large as address count."); + } + + // Convert byte array to uint array in network byte order + var uint32Addrs = new uint[count]; + for (int i = 0; i < count; i++) + { + int offset = i * 4; + uint32Addrs[i] = (uint)(addresses[offset] << 24 | addresses[offset + 1] << 16 | + addresses[offset + 2] << 8 | addresses[offset + 3]); + } + + LookupBatch(uint32Addrs, nextHops); + } + + // ============================================================================ + // Convenience Methods + // ============================================================================ + + /// + /// Converts an IPv4 address string to network byte order uint. + /// + /// The IPv4 address string. + /// The address in network byte order. + public static uint ParseIPv4Address(string address) + { + if (!IPAddress.TryParse(address, out var ip) || + ip.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork) + { + throw new ArgumentException($"Invalid IPv4 address: {address}", nameof(address)); + } + + var bytes = ip.GetAddressBytes(); + return BinaryPrimitives.ReadUInt32BigEndian(bytes); + } + + /// + /// Converts an IPv4 address from byte array to network byte order uint. + /// + /// The IPv4 address bytes. + /// The address in network byte order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint BytesToUInt32(byte[] bytes) + { + if (bytes == null || bytes.Length != 4) + { + throw new ArgumentException("IPv4 address must be 4 bytes.", nameof(bytes)); + } + return BinaryPrimitives.ReadUInt32BigEndian(bytes); + } + + /// + /// Converts a network byte order uint to IPv4 address bytes. + /// + /// The address in network byte order. + /// The IPv4 address bytes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte[] UInt32ToBytes(uint address) + { + var bytes = new byte[4]; + BinaryPrimitives.WriteUInt32BigEndian(bytes, address); + return bytes; + } + + /// + /// Converts a network byte order uint to IPv4 address bytes (Span version). + /// + /// The address in network byte order. + /// The destination span. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void UInt32ToBytes(uint address, Span destination) + { + if (destination.Length < 4) + { + throw new ArgumentException("Destination must be at least 4 bytes.", nameof(destination)); + } + BinaryPrimitives.WriteUInt32BigEndian(destination, address); + } + } +} diff --git a/bindings/csharp/LibLpm/LpmTrieIPv6.cs b/bindings/csharp/LibLpm/LpmTrieIPv6.cs new file mode 100644 index 0000000..dacfc30 --- /dev/null +++ b/bindings/csharp/LibLpm/LpmTrieIPv6.cs @@ -0,0 +1,310 @@ +// LpmTrieIPv6.cs - IPv6-specific LPM trie wrapper +// Provides optimized IPv6 operations and convenience methods + +using System; +using System.Net; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace LibLpm +{ + /// + /// IPv6-specific LPM (Longest Prefix Match) trie. + /// Provides optimized operations for IPv6 address lookups. + /// + /// + /// This class is not thread-safe. For concurrent access, use external synchronization. + /// Implements IDisposable for deterministic resource cleanup. + /// + public sealed class LpmTrieIPv6 : LpmTrie + { + private readonly LpmAlgorithm _algorithm; + + /// + /// Initializes a new instance of the class. + /// + /// The native handle to wrap. + /// The algorithm used for this trie. + private LpmTrieIPv6(SafeLpmHandle handle, LpmAlgorithm algorithm) : base(handle) + { + _algorithm = algorithm; + } + + /// + public override bool IsIPv6 => true; + + /// + /// Gets the algorithm used for this trie. + /// + public LpmAlgorithm Algorithm => _algorithm; + + // ============================================================================ + // Factory Methods + // ============================================================================ + + /// + /// Creates a new IPv6 LPM trie using the default algorithm (Wide16). + /// + /// A new IPv6 LPM trie. + /// Thrown if the trie creation fails. + public static LpmTrieIPv6 CreateDefault() + { + NativeLibraryLoader.Initialize(); + var ptr = NativeMethods.lpm_create_ipv6(); + if (ptr == IntPtr.Zero) + { + throw new LpmCreationException(isIPv6: true, LpmAlgorithm.Default); + } + return new LpmTrieIPv6(new SafeLpmHandle(ptr), LpmAlgorithm.Default); + } + + /// + /// Creates a new IPv6 LPM trie using Wide 16-bit stride algorithm explicitly. + /// + /// A new IPv6 LPM trie with Wide16 algorithm. + /// Thrown if the trie creation fails. + public static LpmTrieIPv6 CreateWide16() + { + NativeLibraryLoader.Initialize(); + var ptr = NativeMethods.lpm_create_ipv6_wide16(); + if (ptr == IntPtr.Zero) + { + throw new LpmCreationException(isIPv6: true, LpmAlgorithm.Wide16); + } + return new LpmTrieIPv6(new SafeLpmHandle(ptr), LpmAlgorithm.Wide16); + } + + /// + /// Creates a new IPv6 LPM trie using 8-bit stride algorithm. + /// + /// A new IPv6 LPM trie with 8-bit stride algorithm. + /// Thrown if the trie creation fails. + public static LpmTrieIPv6 CreateStride8() + { + NativeLibraryLoader.Initialize(); + var ptr = NativeMethods.lpm_create_ipv6_8stride(); + if (ptr == IntPtr.Zero) + { + throw new LpmCreationException(isIPv6: true, LpmAlgorithm.Stride8); + } + return new LpmTrieIPv6(new SafeLpmHandle(ptr), LpmAlgorithm.Stride8); + } + + /// + /// Creates a new IPv6 LPM trie with the specified algorithm. + /// + /// The algorithm to use. + /// A new IPv6 LPM trie. + /// Thrown if the trie creation fails. + /// Thrown if an IPv4-only algorithm is specified. + public static LpmTrieIPv6 Create(LpmAlgorithm algorithm) + { + return algorithm switch + { + LpmAlgorithm.Default => CreateDefault(), + LpmAlgorithm.Wide16 => CreateWide16(), + LpmAlgorithm.Stride8 => CreateStride8(), + LpmAlgorithm.Dir24 => throw new ArgumentException( + "Dir24 algorithm is only valid for IPv4 tries.", nameof(algorithm)), + _ => throw new ArgumentOutOfRangeException(nameof(algorithm)) + }; + } + + // ============================================================================ + // Lookup Operations + // ============================================================================ + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override uint? Lookup(byte[] address) + { + ThrowIfDisposed(); + ValidateAddress(address); + + uint result = NativeMethods.lpm_lookup_ipv6(_handle.DangerousGetHandle(), address); + return result == LpmConstants.InvalidNextHop ? null : result; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override unsafe uint? Lookup(ReadOnlySpan address) + { + ThrowIfDisposed(); + if (address.Length != 16) + { + throw new ArgumentException("IPv6 address must be 16 bytes.", nameof(address)); + } + + fixed (byte* ptr = address) + { + uint result = NativeMethods.lpm_lookup_ipv6(_handle.DangerousGetHandle(), ptr); + return result == LpmConstants.InvalidNextHop ? null : result; + } + } + + /// + /// Performs a raw lookup returning the raw next hop value (including InvalidNextHop). + /// This is the fastest lookup method with no null checking overhead. + /// + /// The IPv6 address (16 bytes). + /// The raw next hop value, or LpmConstants.InvalidNextHop if no match. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public uint LookupRaw(byte[] address) + { + ThrowIfDisposed(); + ValidateAddress(address); + return NativeMethods.lpm_lookup_ipv6(_handle.DangerousGetHandle(), address); + } + + /// + /// Performs a raw lookup returning the raw next hop value (Span version). + /// + /// The IPv6 address (16 bytes). + /// The raw next hop value, or LpmConstants.InvalidNextHop if no match. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe uint LookupRaw(ReadOnlySpan address) + { + ThrowIfDisposed(); + if (address.Length != 16) + { + throw new ArgumentException("IPv6 address must be 16 bytes.", nameof(address)); + } + + fixed (byte* ptr = address) + { + return NativeMethods.lpm_lookup_ipv6(_handle.DangerousGetHandle(), ptr); + } + } + + // ============================================================================ + // Batch Operations + // ============================================================================ + + /// + /// Performs batch lookups for multiple IPv6 addresses. + /// + /// Array of IPv6 addresses (each 16 bytes, contiguous). + /// Output array for next hop values. + /// Thrown if addresses or nextHops is null. + /// Thrown if array sizes are invalid. + /// Thrown if the trie has been disposed. + public unsafe void LookupBatch(byte[] addresses, uint[] nextHops) + { + ThrowIfDisposed(); + if (addresses == null) throw new ArgumentNullException(nameof(addresses)); + if (nextHops == null) throw new ArgumentNullException(nameof(nextHops)); + if (addresses.Length % 16 != 0) + { + throw new ArgumentException("Addresses array length must be a multiple of 16.", nameof(addresses)); + } + + int count = addresses.Length / 16; + if (count > nextHops.Length) + { + throw new ArgumentException("Output array must be at least as large as address count."); + } + + fixed (byte* addrPtr = addresses) + fixed (uint* nhPtr = nextHops) + { + NativeMethods.lpm_lookup_batch_ipv6( + _handle.DangerousGetHandle(), + addrPtr, + nhPtr, + (UIntPtr)count); + } + } + + /// + /// Performs batch lookups for multiple IPv6 addresses (Span version). + /// + /// Span of IPv6 addresses (each 16 bytes, contiguous). + /// Output span for next hop values. + /// Thrown if sizes are invalid. + /// Thrown if the trie has been disposed. + public unsafe void LookupBatch(ReadOnlySpan addresses, Span nextHops) + { + ThrowIfDisposed(); + if (addresses.Length % 16 != 0) + { + throw new ArgumentException("Addresses span length must be a multiple of 16.", nameof(addresses)); + } + + int count = addresses.Length / 16; + if (count > nextHops.Length) + { + throw new ArgumentException("Output span must be at least as large as address count."); + } + + fixed (byte* addrPtr = addresses) + fixed (uint* nhPtr = nextHops) + { + NativeMethods.lpm_lookup_batch_ipv6( + _handle.DangerousGetHandle(), + addrPtr, + nhPtr, + (UIntPtr)count); + } + } + + // ============================================================================ + // Convenience Methods + // ============================================================================ + + /// + /// Parses an IPv6 address string to bytes. + /// + /// The IPv6 address string. + /// The address as 16 bytes. + public static byte[] ParseIPv6Address(string address) + { + if (!IPAddress.TryParse(address, out var ip) || + ip.AddressFamily != System.Net.Sockets.AddressFamily.InterNetworkV6) + { + throw new ArgumentException($"Invalid IPv6 address: {address}", nameof(address)); + } + + return ip.GetAddressBytes(); + } + + /// + /// Formats an IPv6 address byte array as a string. + /// + /// The IPv6 address bytes (16 bytes). + /// The formatted IPv6 address string. + public static string FormatIPv6Address(byte[] bytes) + { + if (bytes == null || bytes.Length != 16) + { + throw new ArgumentException("IPv6 address must be 16 bytes.", nameof(bytes)); + } + + return new IPAddress(bytes).ToString(); + } + + /// + /// Formats an IPv6 address from a span as a string. + /// + /// The IPv6 address bytes (16 bytes). + /// The formatted IPv6 address string. + public static string FormatIPv6Address(ReadOnlySpan bytes) + { + if (bytes.Length != 16) + { + throw new ArgumentException("IPv6 address must be 16 bytes.", nameof(bytes)); + } + + return new IPAddress(bytes.ToArray()).ToString(); + } + + /// + /// Creates a zero-filled IPv6 address buffer. + /// + /// A new 16-byte array filled with zeros. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte[] CreateAddressBuffer() + { + return new byte[16]; + } + } +} diff --git a/bindings/csharp/LibLpm/NativeLibraryLoader.cs b/bindings/csharp/LibLpm/NativeLibraryLoader.cs new file mode 100644 index 0000000..a531e37 --- /dev/null +++ b/bindings/csharp/LibLpm/NativeLibraryLoader.cs @@ -0,0 +1,369 @@ +// NativeLibraryLoader.cs - Cross-platform native library loading +// Handles discovery and loading of the native liblpm library + +using System; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; + +namespace LibLpm +{ + /// + /// Handles cross-platform native library discovery and loading. + /// + public static class NativeLibraryLoader + { + private static readonly object _initLock = new object(); + private static bool _initialized = false; + private static IntPtr _libraryHandle = IntPtr.Zero; + + /// + /// Gets the runtime identifier for the current platform. + /// + public static string RuntimeIdentifier + { + get + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return RuntimeInformation.ProcessArchitecture switch + { + Architecture.X64 => "win-x64", + Architecture.X86 => "win-x86", + Architecture.Arm64 => "win-arm64", + _ => "win-x64" + }; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return RuntimeInformation.ProcessArchitecture switch + { + Architecture.X64 => "linux-x64", + Architecture.Arm64 => "linux-arm64", + Architecture.Arm => "linux-arm", + _ => "linux-x64" + }; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return RuntimeInformation.ProcessArchitecture switch + { + Architecture.X64 => "osx-x64", + Architecture.Arm64 => "osx-arm64", + _ => "osx-x64" + }; + } + else + { + throw new PlatformNotSupportedException($"Unsupported platform: {RuntimeInformation.OSDescription}"); + } + } + } + + /// + /// Gets the native library file name for the current platform. + /// + public static string NativeLibraryName + { + get + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return "lpm.dll"; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return "liblpm.dylib"; + } + else + { + return "liblpm.so"; + } + } + } + + /// + /// Initializes the native library resolver. + /// Call this early in your application startup to ensure the native library can be found. + /// + public static void Initialize() + { + if (_initialized) return; + + lock (_initLock) + { + if (_initialized) return; + + // Try to set up the DLL import resolver if available (requires .NET Core 3.0+) + TrySetupDllImportResolver(); + _initialized = true; + } + } + + /// + /// Tries to set up the DLL import resolver using reflection to avoid compile-time dependency. + /// + private static void TrySetupDllImportResolver() + { + try + { + // Use reflection to access NativeLibrary.SetDllImportResolver if available + var nativeLibraryType = Type.GetType("System.Runtime.InteropServices.NativeLibrary, System.Runtime.InteropServices.RuntimeInformation") + ?? Type.GetType("System.Runtime.InteropServices.NativeLibrary, System.Runtime.InteropServices") + ?? Type.GetType("System.Runtime.InteropServices.NativeLibrary, System.Private.CoreLib") + ?? Type.GetType("System.Runtime.InteropServices.NativeLibrary"); + + if (nativeLibraryType == null) + { + // NativeLibrary not available, rely on default P/Invoke behavior + return; + } + + // Store the type for later use + _nativeLibraryType = nativeLibraryType; + + // Get SetDllImportResolver method + var setResolverMethod = nativeLibraryType.GetMethod("SetDllImportResolver", + BindingFlags.Public | BindingFlags.Static); + + if (setResolverMethod != null) + { + // Create delegate type dynamically + var delegateType = setResolverMethod.GetParameters()[1].ParameterType; + var resolverDelegate = Delegate.CreateDelegate(delegateType, typeof(NativeLibraryLoader), + nameof(DllImportResolverImpl)); + + setResolverMethod.Invoke(null, new object[] { typeof(NativeLibraryLoader).Assembly, resolverDelegate }); + } + } + catch + { + // Ignore errors - fall back to default P/Invoke behavior + } + } + + private static Type? _nativeLibraryType; + + /// + /// DLL import resolver implementation called via reflection. + /// + private static IntPtr DllImportResolverImpl(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) + { + if (libraryName != NativeMethods.LibraryName) + { + return IntPtr.Zero; + } + + // Try to use cached handle + if (_libraryHandle != IntPtr.Zero) + { + return _libraryHandle; + } + + IntPtr handle = IntPtr.Zero; + + // Try each search path + foreach (var path in GetSearchPaths()) + { + if (TryLoadLibraryReflection(path, out handle)) + { + _libraryHandle = handle; + return handle; + } + } + + // Fall back to system library search via reflection + if (TryLoadLibraryWithAssemblyReflection(libraryName, assembly, searchPath, out handle)) + { + _libraryHandle = handle; + return handle; + } + + // Try loading just the library name (system paths) + if (TryLoadLibraryReflection(NativeLibraryName, out handle)) + { + _libraryHandle = handle; + return handle; + } + + // Return IntPtr.Zero to let the default resolver try + return IntPtr.Zero; + } + + /// + /// Attempts to load a native library from the specified path using reflection. + /// + private static bool TryLoadLibraryReflection(string path, out IntPtr handle) + { + handle = IntPtr.Zero; + + if (string.IsNullOrEmpty(path)) + { + return false; + } + + // Check if file exists (for absolute paths) + if (Path.IsPathRooted(path) && !File.Exists(path)) + { + return false; + } + + try + { + if (_nativeLibraryType == null) return false; + + var tryLoadMethod = _nativeLibraryType.GetMethod("TryLoad", + new[] { typeof(string), typeof(IntPtr).MakeByRefType() }); + + if (tryLoadMethod == null) return false; + + var args = new object?[] { path, IntPtr.Zero }; + var result = (bool)tryLoadMethod.Invoke(null, args)!; + if (result) + { + handle = (IntPtr)args[1]!; + } + return result; + } + catch + { + return false; + } + } + + /// + /// Attempts to load a native library with assembly context using reflection. + /// + private static bool TryLoadLibraryWithAssemblyReflection(string libraryName, Assembly assembly, + DllImportSearchPath? searchPath, out IntPtr handle) + { + handle = IntPtr.Zero; + + try + { + if (_nativeLibraryType == null) return false; + + var tryLoadMethod = _nativeLibraryType.GetMethod("TryLoad", + new[] { typeof(string), typeof(Assembly), typeof(DllImportSearchPath?), typeof(IntPtr).MakeByRefType() }); + + if (tryLoadMethod == null) return false; + + var args = new object?[] { libraryName, assembly, searchPath, IntPtr.Zero }; + var result = (bool)tryLoadMethod.Invoke(null, args)!; + if (result) + { + handle = (IntPtr)args[3]!; + } + return result; + } + catch + { + return false; + } + } + + /// + /// Gets the list of paths to search for the native library. + /// + public static string[] GetSearchPaths() + { + var paths = new System.Collections.Generic.List(); + var rid = RuntimeIdentifier; + var libName = NativeLibraryName; + + // Get the directory containing this assembly + var assemblyDir = Path.GetDirectoryName(typeof(NativeLibraryLoader).Assembly.Location); + if (!string.IsNullOrEmpty(assemblyDir)) + { + // 1. runtimes/{rid}/native/{libname} relative to assembly + paths.Add(Path.Combine(assemblyDir, "runtimes", rid, "native", libName)); + + // 2. Same directory as assembly + paths.Add(Path.Combine(assemblyDir, libName)); + + // 3. Native subdirectory + paths.Add(Path.Combine(assemblyDir, "native", libName)); + } + + // 4. Current directory + paths.Add(Path.Combine(Environment.CurrentDirectory, libName)); + paths.Add(Path.Combine(Environment.CurrentDirectory, "runtimes", rid, "native", libName)); + + // 5. App base directory + var baseDir = AppContext.BaseDirectory; + if (!string.IsNullOrEmpty(baseDir)) + { + paths.Add(Path.Combine(baseDir, "runtimes", rid, "native", libName)); + paths.Add(Path.Combine(baseDir, libName)); + } + + // 6. LD_LIBRARY_PATH / DYLD_LIBRARY_PATH directories (Linux/macOS) + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var ldPath = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) + ? Environment.GetEnvironmentVariable("DYLD_LIBRARY_PATH") + : Environment.GetEnvironmentVariable("LD_LIBRARY_PATH"); + + if (!string.IsNullOrEmpty(ldPath)) + { + foreach (var dir in ldPath.Split(':')) + { + if (!string.IsNullOrEmpty(dir)) + { + paths.Add(Path.Combine(dir, libName)); + } + } + } + + // 7. Standard system paths (Linux) + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + paths.Add($"/usr/local/lib/{libName}"); + paths.Add($"/usr/lib/{libName}"); + paths.Add($"/usr/lib/x86_64-linux-gnu/{libName}"); + paths.Add($"/lib/x86_64-linux-gnu/{libName}"); + } + } + + return paths.ToArray(); + } + + /// + /// Attempts to load the native library and returns the first path that works. + /// + /// The path to the loaded library, or null if not found. + public static string? FindLibraryPath() + { + foreach (var path in GetSearchPaths()) + { + if (File.Exists(path)) + { + return path; + } + } + return null; + } + + /// + /// Gets the library version string from the native library. + /// + /// The version string, or null if the library is not loaded. + public static string? GetVersion() + { + try + { + Initialize(); + var ptr = NativeMethods.lpm_get_version(); + if (ptr == IntPtr.Zero) + { + return null; + } + return Marshal.PtrToStringAnsi(ptr); + } + catch + { + return null; + } + } + } +} diff --git a/bindings/csharp/LibLpm/NativeMethods.cs b/bindings/csharp/LibLpm/NativeMethods.cs new file mode 100644 index 0000000..fac9e35 --- /dev/null +++ b/bindings/csharp/LibLpm/NativeMethods.cs @@ -0,0 +1,456 @@ +// NativeMethods.cs - P/Invoke declarations for liblpm C library +// This file contains all native function declarations and constants + +using System; +using System.Runtime.InteropServices; + +namespace LibLpm +{ + /// + /// Constants from lpm.h + /// + public static class LpmConstants + { + /// + /// Invalid next hop value returned when no match is found. + /// + public const uint InvalidNextHop = 0xFFFFFFFF; + + /// + /// Invalid index value. + /// + public const uint InvalidIndex = 0; + + /// + /// Maximum prefix depth for IPv4 addresses. + /// + public const byte IPv4MaxDepth = 32; + + /// + /// Maximum prefix depth for IPv6 addresses. + /// + public const byte IPv6MaxDepth = 128; + + /// + /// Cache line size for alignment. + /// + public const int CacheLineSize = 64; + + /// + /// 8-bit stride size (256 entries per node). + /// + public const int StrideBits8 = 8; + public const int StrideSize8 = 256; + + /// + /// 16-bit stride size (65536 entries per node). + /// + public const int StrideBits16 = 16; + public const int StrideSize16 = 65536; + + /// + /// IPv4 DIR-24-8 configuration. + /// + public const int IPv4Dir24Bits = 24; + public const int IPv4Dir24Size = 16777216; // 1 << 24 + } + + /// + /// Native P/Invoke methods for liblpm. + /// + internal static class NativeMethods + { + /// + /// Library name for P/Invoke. Platform-specific resolution is handled by NativeLibraryLoader. + /// + public const string LibraryName = "lpm"; + + // ============================================================================ + // GENERIC API (dispatches to compile-time selected algorithm) + // ============================================================================ + + /// + /// Creates a new IPv4 LPM trie using the default algorithm (DIR-24-8). + /// + /// Pointer to the created trie, or IntPtr.Zero on failure. + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr lpm_create_ipv4(); + + /// + /// Creates a new IPv6 LPM trie using the default algorithm (Wide16). + /// + /// Pointer to the created trie, or IntPtr.Zero on failure. + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr lpm_create_ipv6(); + + /// + /// Destroys an LPM trie and frees all associated memory. + /// + /// Pointer to the trie to destroy. Safe to call with IntPtr.Zero. + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern void lpm_destroy(IntPtr trie); + + /// + /// Adds a prefix to the LPM trie. + /// + /// Pointer to the trie. + /// Prefix bytes (4 for IPv4, 16 for IPv6). + /// Prefix length in bits. + /// Next hop value to associate with the prefix. + /// 0 on success, -1 on failure. + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern int lpm_add(IntPtr trie, byte[] prefix, byte prefix_len, uint next_hop); + + /// + /// Adds a prefix to the LPM trie (unsafe version for Span support). + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern unsafe int lpm_add(IntPtr trie, byte* prefix, byte prefix_len, uint next_hop); + + /// + /// Deletes a prefix from the LPM trie. + /// + /// Pointer to the trie. + /// Prefix bytes (4 for IPv4, 16 for IPv6). + /// Prefix length in bits. + /// 0 on success, -1 on failure (including prefix not found). + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern int lpm_delete(IntPtr trie, byte[] prefix, byte prefix_len); + + /// + /// Deletes a prefix from the LPM trie (unsafe version for Span support). + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern unsafe int lpm_delete(IntPtr trie, byte* prefix, byte prefix_len); + + /// + /// Performs an IPv4 lookup in the trie. + /// + /// Pointer to the trie. + /// IPv4 address as a 32-bit integer (network byte order). + /// Next hop value on match, LPM_INVALID_NEXT_HOP if no match. + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern uint lpm_lookup_ipv4(IntPtr trie, uint addr); + + /// + /// Performs an IPv6 lookup in the trie. + /// + /// Pointer to the trie. + /// IPv6 address as 16 bytes. + /// Next hop value on match, LPM_INVALID_NEXT_HOP if no match. + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern uint lpm_lookup_ipv6(IntPtr trie, byte[] addr); + + /// + /// Performs an IPv6 lookup in the trie (unsafe version for Span support). + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern unsafe uint lpm_lookup_ipv6(IntPtr trie, byte* addr); + + /// + /// Performs a batch IPv4 lookup. + /// + /// Pointer to the trie. + /// Array of IPv4 addresses (network byte order). + /// Output array for next hop values. + /// Number of addresses to look up. + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern void lpm_lookup_batch_ipv4(IntPtr trie, uint[] addrs, uint[] next_hops, UIntPtr count); + + /// + /// Performs a batch IPv4 lookup (unsafe version). + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern unsafe void lpm_lookup_batch_ipv4(IntPtr trie, uint* addrs, uint* next_hops, UIntPtr count); + + /// + /// Performs a batch IPv6 lookup. + /// + /// Pointer to the trie. + /// Array of IPv6 addresses (each 16 bytes, contiguous). + /// Output array for next hop values. + /// Number of addresses to look up. + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern unsafe void lpm_lookup_batch_ipv6(IntPtr trie, byte* addrs, uint* next_hops, UIntPtr count); + + // ============================================================================ + // ALGORITHM-SPECIFIC API: IPv4 DIR-24-8 + // ============================================================================ + + /// + /// Creates a new IPv4 LPM trie using DIR-24-8 algorithm explicitly. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr lpm_create_ipv4_dir24(); + + /// + /// Adds a prefix using DIR-24-8 algorithm. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern int lpm_add_ipv4_dir24(IntPtr trie, byte[] prefix, byte prefix_len, uint next_hop); + + /// + /// Adds a prefix using DIR-24-8 algorithm (unsafe version). + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern unsafe int lpm_add_ipv4_dir24(IntPtr trie, byte* prefix, byte prefix_len, uint next_hop); + + /// + /// Deletes a prefix using DIR-24-8 algorithm. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern int lpm_delete_ipv4_dir24(IntPtr trie, byte[] prefix, byte prefix_len); + + /// + /// Deletes a prefix using DIR-24-8 algorithm (unsafe version). + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern unsafe int lpm_delete_ipv4_dir24(IntPtr trie, byte* prefix, byte prefix_len); + + /// + /// Performs an IPv4 lookup using DIR-24-8 algorithm. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern uint lpm_lookup_ipv4_dir24(IntPtr trie, uint addr); + + /// + /// Performs an IPv4 lookup using DIR-24-8 algorithm (byte array version). + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern uint lpm_lookup_ipv4_dir24_bytes(IntPtr trie, byte[] addr); + + /// + /// Performs an IPv4 lookup using DIR-24-8 algorithm (byte array version, unsafe). + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern unsafe uint lpm_lookup_ipv4_dir24_bytes(IntPtr trie, byte* addr); + + /// + /// Performs a batch IPv4 lookup using DIR-24-8 algorithm. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern void lpm_lookup_batch_ipv4_dir24(IntPtr trie, uint[] addrs, uint[] next_hops, UIntPtr count); + + /// + /// Performs a batch IPv4 lookup using DIR-24-8 algorithm (unsafe version). + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern unsafe void lpm_lookup_batch_ipv4_dir24(IntPtr trie, uint* addrs, uint* next_hops, UIntPtr count); + + /// + /// Performs a batch IPv4 lookup using DIR-24-8 algorithm (byte array version, unsafe). + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern unsafe void lpm_lookup_batch_ipv4_dir24_bytes(IntPtr trie, byte* addrs, uint* next_hops, UIntPtr count); + + // ============================================================================ + // ALGORITHM-SPECIFIC API: IPv4 8-bit Stride + // ============================================================================ + + /// + /// Creates a new IPv4 LPM trie using 8-bit stride algorithm. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr lpm_create_ipv4_8stride(); + + /// + /// Adds a prefix using 8-bit stride algorithm. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern int lpm_add_ipv4_8stride(IntPtr trie, byte[] prefix, byte prefix_len, uint next_hop); + + /// + /// Adds a prefix using 8-bit stride algorithm (unsafe version). + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern unsafe int lpm_add_ipv4_8stride(IntPtr trie, byte* prefix, byte prefix_len, uint next_hop); + + /// + /// Deletes a prefix using 8-bit stride algorithm. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern int lpm_delete_ipv4_8stride(IntPtr trie, byte[] prefix, byte prefix_len); + + /// + /// Deletes a prefix using 8-bit stride algorithm (unsafe version). + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern unsafe int lpm_delete_ipv4_8stride(IntPtr trie, byte* prefix, byte prefix_len); + + /// + /// Performs an IPv4 lookup using 8-bit stride algorithm. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern uint lpm_lookup_ipv4_8stride(IntPtr trie, uint addr); + + /// + /// Performs an IPv4 lookup using 8-bit stride algorithm (byte array version). + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern uint lpm_lookup_ipv4_8stride_bytes(IntPtr trie, byte[] addr); + + /// + /// Performs an IPv4 lookup using 8-bit stride algorithm (byte array version, unsafe). + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern unsafe uint lpm_lookup_ipv4_8stride_bytes(IntPtr trie, byte* addr); + + /// + /// Performs a batch IPv4 lookup using 8-bit stride algorithm. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern void lpm_lookup_batch_ipv4_8stride(IntPtr trie, uint[] addrs, uint[] next_hops, UIntPtr count); + + /// + /// Performs a batch IPv4 lookup using 8-bit stride algorithm (unsafe version). + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern unsafe void lpm_lookup_batch_ipv4_8stride(IntPtr trie, uint* addrs, uint* next_hops, UIntPtr count); + + // ============================================================================ + // ALGORITHM-SPECIFIC API: IPv6 Wide 16-bit Stride + // ============================================================================ + + /// + /// Creates a new IPv6 LPM trie using wide 16-bit stride algorithm. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr lpm_create_ipv6_wide16(); + + /// + /// Adds a prefix using wide 16-bit stride algorithm. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern int lpm_add_ipv6_wide16(IntPtr trie, byte[] prefix, byte prefix_len, uint next_hop); + + /// + /// Adds a prefix using wide 16-bit stride algorithm (unsafe version). + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern unsafe int lpm_add_ipv6_wide16(IntPtr trie, byte* prefix, byte prefix_len, uint next_hop); + + /// + /// Deletes a prefix using wide 16-bit stride algorithm. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern int lpm_delete_ipv6_wide16(IntPtr trie, byte[] prefix, byte prefix_len); + + /// + /// Deletes a prefix using wide 16-bit stride algorithm (unsafe version). + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern unsafe int lpm_delete_ipv6_wide16(IntPtr trie, byte* prefix, byte prefix_len); + + /// + /// Performs an IPv6 lookup using wide 16-bit stride algorithm. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern uint lpm_lookup_ipv6_wide16(IntPtr trie, byte[] addr); + + /// + /// Performs an IPv6 lookup using wide 16-bit stride algorithm (unsafe version). + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern unsafe uint lpm_lookup_ipv6_wide16(IntPtr trie, byte* addr); + + /// + /// Performs a batch IPv6 lookup using wide 16-bit stride algorithm. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern unsafe void lpm_lookup_batch_ipv6_wide16(IntPtr trie, byte* addrs, uint* next_hops, UIntPtr count); + + // ============================================================================ + // ALGORITHM-SPECIFIC API: IPv6 8-bit Stride + // ============================================================================ + + /// + /// Creates a new IPv6 LPM trie using 8-bit stride algorithm. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr lpm_create_ipv6_8stride(); + + /// + /// Adds a prefix using 8-bit stride algorithm. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern int lpm_add_ipv6_8stride(IntPtr trie, byte[] prefix, byte prefix_len, uint next_hop); + + /// + /// Adds a prefix using 8-bit stride algorithm (unsafe version). + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern unsafe int lpm_add_ipv6_8stride(IntPtr trie, byte* prefix, byte prefix_len, uint next_hop); + + /// + /// Deletes a prefix using 8-bit stride algorithm. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern int lpm_delete_ipv6_8stride(IntPtr trie, byte[] prefix, byte prefix_len); + + /// + /// Deletes a prefix using 8-bit stride algorithm (unsafe version). + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern unsafe int lpm_delete_ipv6_8stride(IntPtr trie, byte* prefix, byte prefix_len); + + /// + /// Performs an IPv6 lookup using 8-bit stride algorithm. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern uint lpm_lookup_ipv6_8stride(IntPtr trie, byte[] addr); + + /// + /// Performs an IPv6 lookup using 8-bit stride algorithm (unsafe version). + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern unsafe uint lpm_lookup_ipv6_8stride(IntPtr trie, byte* addr); + + /// + /// Performs a batch IPv6 lookup using 8-bit stride algorithm. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern unsafe void lpm_lookup_batch_ipv6_8stride(IntPtr trie, byte* addrs, uint* next_hops, UIntPtr count); + + // ============================================================================ + // LEGACY API + // ============================================================================ + + /// + /// Creates a new LPM trie with specified max depth (legacy API). + /// + /// Maximum prefix depth (32 for IPv4, 128 for IPv6). + /// Pointer to the created trie, or IntPtr.Zero on failure. + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr lpm_create(byte max_depth); + + /// + /// Performs a lookup in the trie (legacy API, auto-detects based on max_depth). + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern uint lpm_lookup(IntPtr trie, byte[] addr); + + /// + /// Performs a lookup in the trie (legacy API, unsafe version). + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern unsafe uint lpm_lookup(IntPtr trie, byte* addr); + + // ============================================================================ + // UTILITY FUNCTIONS + // ============================================================================ + + /// + /// Gets the library version string. + /// + /// Pointer to a null-terminated version string. + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr lpm_get_version(); + + /// + /// Prints trie statistics to stdout. + /// + /// Pointer to the trie. + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern void lpm_print_stats(IntPtr trie); + } +} diff --git a/bindings/csharp/LibLpm/SafeLpmHandle.cs b/bindings/csharp/LibLpm/SafeLpmHandle.cs new file mode 100644 index 0000000..896aefe --- /dev/null +++ b/bindings/csharp/LibLpm/SafeLpmHandle.cs @@ -0,0 +1,85 @@ +// SafeLpmHandle.cs - Safe handle for native LPM trie resources +// Ensures proper cleanup of native memory even in case of exceptions + +using System; +using System.Runtime.InteropServices; + +namespace LibLpm +{ + /// + /// A safe handle wrapper for the native lpm_trie_t pointer. + /// Ensures that lpm_destroy() is always called when the handle is released, + /// even if an exception occurs or the object is garbage collected. + /// + public sealed class SafeLpmHandle : SafeHandle + { + /// + /// Creates a new invalid SafeLpmHandle. + /// + public SafeLpmHandle() : base(IntPtr.Zero, ownsHandle: true) + { + } + + /// + /// Creates a SafeLpmHandle wrapping the specified native pointer. + /// + /// The native lpm_trie_t pointer. + /// Whether this handle owns the native resource. + internal SafeLpmHandle(IntPtr handle, bool ownsHandle = true) : base(IntPtr.Zero, ownsHandle) + { + SetHandle(handle); + } + + /// + /// Gets a value indicating whether the handle value is invalid. + /// A handle is invalid if it is IntPtr.Zero. + /// + public override bool IsInvalid => handle == IntPtr.Zero; + + /// + /// Gets the underlying native pointer. + /// + /// + /// Use this property when you need to pass the handle to native methods. + /// Prefer using DangerousGetHandle() for explicit P/Invoke calls. + /// + public IntPtr Handle => handle; + + /// + /// Releases the native handle by calling lpm_destroy(). + /// + /// True if the handle was released successfully. + protected override bool ReleaseHandle() + { + if (handle != IntPtr.Zero) + { + NativeMethods.lpm_destroy(handle); + handle = IntPtr.Zero; + } + return true; + } + + /// + /// Creates an invalid (null) handle. + /// + public static SafeLpmHandle Invalid => new SafeLpmHandle(); + + /// + /// Explicitly converts an IntPtr to a SafeLpmHandle. + /// + /// The native pointer. + public static explicit operator SafeLpmHandle(IntPtr ptr) + { + return new SafeLpmHandle(ptr); + } + + /// + /// Gets the native pointer from the safe handle. + /// + /// The safe handle. + public static implicit operator IntPtr(SafeLpmHandle handle) + { + return handle?.handle ?? IntPtr.Zero; + } + } +} diff --git a/bindings/csharp/README.md b/bindings/csharp/README.md new file mode 100644 index 0000000..26cbc91 --- /dev/null +++ b/bindings/csharp/README.md @@ -0,0 +1,467 @@ +# C# Bindings for liblpm + +C# wrapper for [liblpm](https://github.com/MuriloChianfa/liblpm), providing high-performance Longest Prefix Match (LPM) operations for IP routing in .NET applications. + +## Features + +- **High Performance**: P/Invoke bindings with minimal overhead +- **.NET Standard 2.1**: Compatible with .NET Core 3.0+, .NET 5/6/7/8+, Unity 2021.2+ +- **Type Safety**: Separate IPv4 and IPv6 APIs with compile-time checks +- **Zero-Copy**: `Span` support for batch operations without allocations +- **Safe Resource Management**: `IDisposable` pattern with `SafeHandle` for automatic cleanup +- **Cross-Platform**: Native library loading for Linux, Windows, and macOS +- **Algorithm Selection**: Choose between different algorithms (DIR-24-8, Wide16, 8-stride) +- **NuGet Package**: Easy installation with bundled native libraries + +## Installation + +### Via NuGet + +```bash +dotnet add package liblpm +``` + +### Building from Source + +Prerequisites: +- .NET SDK 8.0 or later +- CMake 3.16+ +- C compiler (GCC, Clang) + +```bash +# Clone the repository +git clone --recursive https://github.com/MuriloChianfa/liblpm.git +cd liblpm + +# Build liblpm native library +mkdir -p build && cd build +cmake -DBUILD_CSHARP_WRAPPER=ON .. +make -j$(nproc) + +# Build and test C# bindings +make csharp_wrapper +make csharp_test +``` + +## Quick Start + +### Basic IPv4 Example + +```csharp +using LibLpm; + +// Create an IPv4 routing table +using var trie = LpmTrieIPv4.CreateDefault(); + +// Add routes +trie.Add("192.168.0.0/16", 100); +trie.Add("10.0.0.0/8", 200); +trie.Add("0.0.0.0/0", 1); // Default route + +// Perform lookups +uint? nextHop = trie.Lookup("192.168.1.1"); +Console.WriteLine($"Next hop: {nextHop}"); // Output: 100 + +// Delete a route +trie.Delete("192.168.0.0/16"); +``` + +### Basic IPv6 Example + +```csharp +using LibLpm; + +// Create an IPv6 routing table +using var trie = LpmTrieIPv6.CreateDefault(); + +// Add routes +trie.Add("2001:db8::/32", 100); +trie.Add("fe80::/10", 200); +trie.Add("::/0", 1); // Default route + +// Perform lookups +uint? nextHop = trie.Lookup("2001:db8::1"); +Console.WriteLine($"Next hop: {nextHop}"); // Output: 100 +``` + +### Fast Path: Byte Array API + +For maximum performance, use byte arrays to avoid string parsing overhead: + +```csharp +using LibLpm; + +using var trie = LpmTrieIPv4.CreateDefault(); + +// Insert with byte array +byte[] prefix = { 192, 168, 0, 0 }; +trie.Add(prefix, prefixLen: 16, nextHop: 100); + +// Lookup with byte array +byte[] addr = { 192, 168, 1, 1 }; +uint? nextHop = trie.Lookup(addr); + +// Even faster: use uint for IPv4 +uint addr32 = 0xC0A80101; // 192.168.1.1 +uint? result = trie.Lookup(addr32); +``` + +### Zero-Copy with Span + +Use `Span` for stack-allocated, zero-copy operations: + +```csharp +using LibLpm; +using System; + +using var trie = LpmTrieIPv4.CreateDefault(); + +// Stack-allocated prefix +Span prefix = stackalloc byte[] { 192, 168, 0, 0 }; +trie.Add(prefix, 16, 100); + +// Stack-allocated lookup +ReadOnlySpan addr = stackalloc byte[] { 192, 168, 1, 1 }; +uint? nextHop = trie.Lookup(addr); +``` + +### Batch Operations + +Process multiple lookups efficiently: + +```csharp +using LibLpm; + +using var trie = LpmTrieIPv4.CreateDefault(); +trie.Add("0.0.0.0/0", 1); + +// Batch lookup with arrays +uint[] addresses = { 0xC0A80101, 0x0A010101, 0x08080808 }; +uint[] results = new uint[3]; +trie.LookupBatch(addresses, results); + +// Batch lookup with Span (zero allocation) +Span addrSpan = stackalloc uint[] { 0xC0A80101, 0x0A010101 }; +Span resultSpan = stackalloc uint[2]; +trie.LookupBatch(addrSpan, resultSpan); +``` + +## API Reference + +### Table Creation + +```csharp +// IPv4 with default algorithm (DIR-24-8) +using var trie = LpmTrieIPv4.CreateDefault(); + +// IPv4 with specific algorithms +using var trieDir24 = LpmTrieIPv4.CreateDir24(); +using var trieStride8 = LpmTrieIPv4.CreateStride8(); + +// IPv6 with default algorithm (Wide16) +using var trieV6 = LpmTrieIPv6.CreateDefault(); + +// IPv6 with specific algorithms +using var trieWide16 = LpmTrieIPv6.CreateWide16(); +using var trieV6Stride8 = LpmTrieIPv6.CreateStride8(); +``` + +### Core Operations + +#### Add Routes + +```csharp +// String-based (convenient) +trie.Add("192.168.0.0/16", nextHop: 100); + +// Byte array (fast) +byte[] prefix = { 192, 168, 0, 0 }; +trie.Add(prefix, prefixLen: 16, nextHop: 100); + +// Span (zero-copy) +ReadOnlySpan spanPrefix = stackalloc byte[] { 192, 168, 0, 0 }; +trie.Add(spanPrefix, 16, 100); + +// TryAdd (no exception on failure) +bool success = trie.TryAdd(prefix, 16, 100); +``` + +#### Lookup + +```csharp +// String-based +uint? nextHop = trie.Lookup("192.168.1.1"); + +// IPAddress +var addr = IPAddress.Parse("192.168.1.1"); +uint? nextHop = trie.Lookup(addr); + +// Byte array +byte[] addrBytes = { 192, 168, 1, 1 }; +uint? nextHop = trie.Lookup(addrBytes); + +// uint (IPv4 only, fastest) +uint addr32 = 0xC0A80101; +uint? nextHop = trie.Lookup(addr32); + +// Raw lookup (no null check, returns InvalidNextHop on miss) +uint rawResult = trie.LookupRaw(addr32); +if (rawResult != LpmConstants.InvalidNextHop) { + // Found +} +``` + +#### Delete Routes + +```csharp +// String-based +bool deleted = trie.Delete("192.168.0.0/16"); + +// Byte array +byte[] prefix = { 192, 168, 0, 0 }; +bool deleted = trie.Delete(prefix, 16); +``` + +### Batch Operations + +```csharp +// IPv4 batch lookup +uint[] addresses = new uint[1000]; +uint[] results = new uint[1000]; +trie.LookupBatch(addresses, results); + +// IPv4 batch with Span +Span addrSpan = stackalloc uint[100]; +Span resultSpan = stackalloc uint[100]; +trie.LookupBatch(addrSpan, resultSpan); + +// IPv6 batch lookup (byte arrays, 16 bytes per address) +byte[] ipv6Addresses = new byte[1600]; // 100 addresses +uint[] ipv6Results = new uint[100]; +trieV6.LookupBatch(ipv6Addresses, ipv6Results); +``` + +### Constants + +```csharp +// Invalid next hop value (returned when no match) +uint invalid = LpmConstants.InvalidNextHop; // 0xFFFFFFFF + +// Maximum prefix lengths +byte ipv4Max = LpmConstants.IPv4MaxDepth; // 32 +byte ipv6Max = LpmConstants.IPv6MaxDepth; // 128 +``` + +### Utility Methods + +```csharp +// Get library version +string? version = LpmTrie.GetVersion(); + +// IPv4 address conversion +uint addr = LpmTrieIPv4.ParseIPv4Address("192.168.1.1"); +byte[] bytes = LpmTrieIPv4.UInt32ToBytes(addr); +uint back = LpmTrieIPv4.BytesToUInt32(bytes); + +// IPv6 address conversion +byte[] ipv6 = LpmTrieIPv6.ParseIPv6Address("2001:db8::1"); +string str = LpmTrieIPv6.FormatIPv6Address(ipv6); +``` + +## Performance Tips + +### 1. Use Byte Array or uint API + +String parsing has overhead. For hot paths, use byte arrays or uint: + +```csharp +// Slower (string parsing) +trie.Lookup("192.168.1.1"); + +// Faster (byte array) +trie.Lookup(new byte[] { 192, 168, 1, 1 }); + +// Fastest (uint, IPv4 only) +trie.Lookup(0xC0A80101); +``` + +### 2. Use Batch Operations + +Batch lookups are more efficient than individual lookups: + +```csharp +// Instead of: +foreach (var addr in addresses) { + results.Add(trie.Lookup(addr)); +} + +// Use: +trie.LookupBatch(addresses, results); +``` + +### 3. Use Span for Zero Allocations + +```csharp +// Stack allocation, no GC pressure +Span addresses = stackalloc uint[100]; +Span results = stackalloc uint[100]; +trie.LookupBatch(addresses, results); +``` + +### 4. Choose the Right Algorithm + +- **DIR-24-8** (IPv4 default): Best for IPv4, 1-2 memory accesses, ~64MB memory +- **Wide16** (IPv6 default): Optimized for IPv6 /48 allocations +- **Stride8**: Memory-efficient for sparse prefix sets + +## Thread Safety + +`LpmTrieIPv4` and `LpmTrieIPv6` are **not thread-safe**. For concurrent access: + +```csharp +// Option 1: External synchronization +private readonly object _lock = new object(); +private readonly LpmTrieIPv4 _trie = LpmTrieIPv4.CreateDefault(); + +public uint? Lookup(uint addr) { + lock (_lock) { + return _trie.Lookup(addr); + } +} + +// Option 2: ReaderWriterLockSlim (multiple readers, single writer) +private readonly ReaderWriterLockSlim _rwLock = new(); +private readonly LpmTrieIPv4 _trie = LpmTrieIPv4.CreateDefault(); + +public uint? Lookup(uint addr) { + _rwLock.EnterReadLock(); + try { + return _trie.Lookup(addr); + } + finally { + _rwLock.ExitReadLock(); + } +} + +public void Add(string cidr, uint nextHop) { + _rwLock.EnterWriteLock(); + try { + _trie.Add(cidr, nextHop); + } + finally { + _rwLock.ExitWriteLock(); + } +} +``` + +## Exception Handling + +```csharp +try { + using var trie = LpmTrieIPv4.CreateDefault(); + trie.Add("invalid", 100); +} +catch (LpmCreationException ex) { + // Failed to create trie (memory allocation failure) +} +catch (LpmInvalidPrefixException ex) { + // Invalid prefix format or length +} +catch (LpmOperationException ex) { + // Add/delete operation failed +} +catch (ObjectDisposedException ex) { + // Trie has been disposed +} +``` + +## Resource Management + +Always dispose tries to release native memory: + +```csharp +// Recommended: using statement +using var trie = LpmTrieIPv4.CreateDefault(); +// ... use trie ... +// Automatically disposed at end of scope + +// Alternative: manual disposal +var trie = LpmTrieIPv4.CreateDefault(); +try { + // ... use trie ... +} +finally { + trie.Dispose(); +} +``` + +## Native Library Loading + +The library automatically searches for the native `liblpm` library in: + +1. `runtimes/{rid}/native/` relative to the assembly +2. Same directory as the assembly +3. System library paths (LD_LIBRARY_PATH, etc.) +4. Standard installation paths (`/usr/lib`, `/usr/local/lib`) + +To verify the library is found: + +```csharp +string? path = NativeLibraryLoader.FindLibraryPath(); +Console.WriteLine($"Native library: {path}"); + +string? version = LpmTrie.GetVersion(); +Console.WriteLine($"Version: {version}"); +``` + +## Building the NuGet Package + +```bash +cd bindings/csharp +dotnet pack -c Release -o ./nupkg + +# The package will be at: ./nupkg/liblpm.2.0.0.nupkg +``` + +## Examples + +See the [LibLpm.Examples](LibLpm.Examples/) directory for complete examples: + +- [BasicExample.cs](LibLpm.Examples/BasicExample.cs) - IPv4/IPv6 operations +- [BatchExample.cs](LibLpm.Examples/BatchExample.cs) - High-performance batch lookups + +Run examples: + +```bash +cd bindings/csharp +dotnet run --project LibLpm.Examples +``` + +## Testing + +```bash +cd bindings/csharp +dotnet test + +# With verbose output +dotnet test --verbosity normal + +# With coverage +dotnet test --collect:"XPlat Code Coverage" +``` + +## Contributing + +Contributions are welcome! Please ensure: + +- All tests pass (`dotnet test`) +- Code follows C# conventions +- XML documentation is provided for public APIs +- Performance is considered + +## See Also + +- [liblpm main documentation](../../README.md) +- [C++ bindings](../cpp/README.md) +- [Go bindings](../go/README.md) +- [C API reference](../../include/lpm.h) diff --git a/docker/Dockerfile.csharp b/docker/Dockerfile.csharp new file mode 100644 index 0000000..745975d --- /dev/null +++ b/docker/Dockerfile.csharp @@ -0,0 +1,68 @@ +# Dockerfile for C# bindings CI testing +# Builds the native liblpm library and tests the C# bindings + +FROM mcr.microsoft.com/dotnet/sdk:8.0 + +# Install C build tools and GCC 13 for C23 support +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + cmake \ + ninja-build \ + git \ + pkg-config \ + libc6-dev \ + software-properties-common \ + gnupg \ + wget \ + && rm -rf /var/lib/apt/lists/* + +# Install GCC 13 from testing repository for C23 support +RUN echo "deb http://deb.debian.org/debian testing main" >> /etc/apt/sources.list && \ + apt-get update && \ + apt-get install -y --no-install-recommends gcc-13 g++-13 && \ + update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-13 100 && \ + update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-13 100 && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /build + +# Copy the entire liblpm project +COPY . /build/ + +# Initialize submodules and build liblpm native library +RUN git config --global --add safe.directory /build && \ + if [ -f .gitmodules ]; then git submodule update --init --recursive; fi && \ + mkdir -p build && cd build && \ + cmake -DCMAKE_BUILD_TYPE=Release -GNinja .. && \ + ninja && \ + ninja install && \ + ldconfig + +# Copy native library to runtimes directory for .NET to find +RUN mkdir -p /build/bindings/csharp/runtimes/linux-x64/native && \ + cp /build/build/liblpm.so /build/bindings/csharp/runtimes/linux-x64/native/ + +# Set library path for runtime +ENV LD_LIBRARY_PATH=/usr/local/lib:/build/build:$LD_LIBRARY_PATH + +# Build and test C# bindings +WORKDIR /build/bindings/csharp + +# Restore dependencies +RUN dotnet restore + +# Build in Release mode +RUN dotnet build --configuration Release --no-restore + +# Copy native library to test output directory and create symlinks for different naming conventions +RUN cp /build/build/liblpm.so /build/bindings/csharp/LibLpm.Tests/bin/Release/net8.0/ && \ + cp /build/build/liblpm.so /build/bindings/csharp/LibLpm.Examples/bin/Release/net8.0/ && \ + ln -sf liblpm.so /build/bindings/csharp/LibLpm.Tests/bin/Release/net8.0/lpm.so && \ + ln -sf liblpm.so /build/bindings/csharp/LibLpm.Examples/bin/Release/net8.0/lpm.so && \ + ln -sf /usr/local/lib/liblpm.so /usr/local/lib/lpm.so + +# Run tests during build to validate +RUN dotnet test --configuration Release --verbosity minimal + +# Default command: run tests again (for CI validation) +CMD ["dotnet", "test", "--configuration", "Release", "--verbosity", "normal"] diff --git a/docker/README.md b/docker/README.md index c9edc06..8fa4b1f 100644 --- a/docker/README.md +++ b/docker/README.md @@ -11,6 +11,7 @@ Quick reference for liblpm Docker images. | `liblpm-test` | Testing environment | Automated testing, CI | | `liblpm-fuzz` | AFL++ fuzzing | Security testing | | `liblpm-cpp` | C++ bindings | C++ wrapper testing | +| `liblpm-csharp` | C# bindings (.NET) | C# wrapper testing | | `liblpm-go` | Go bindings | Go wrapper testing | | `liblpm-benchmark` | DPDK benchmarking | Performance comparison | | `liblpm-deb` | DEB package builder | Building Debian/Ubuntu packages | @@ -77,8 +78,15 @@ docker run --rm -v "$PWD/fuzz_output:/fuzz/fuzz_output" --cpus=4 liblpm-fuzz # Test C++ bindings docker run --rm liblpm-cpp +# Test C# bindings (.NET) +docker run --rm liblpm-csharp + # Test Go bindings docker run --rm liblpm-go + +# Extract C# NuGet package +docker run --rm -v "$PWD/artifacts:/artifacts" liblpm-csharp \ + bash -c "cd /build/bindings/csharp && dotnet pack -o /artifacts" ``` ### Benchmarking @@ -186,6 +194,33 @@ C++17 environment for building and testing C++ bindings. docker run --rm liblpm-cpp ``` +### liblpm-csharp + +.NET 8.0 environment for building and testing C# bindings. + +**Size:** ~700MB + +**Features:** +- .NET SDK 8.0 +- P/Invoke bindings with SafeHandle +- xUnit tests +- NuGet packaging ready + +```bash +# Run tests +docker run --rm liblpm-csharp + +# Interactive development +docker run -it --rm liblpm-csharp bash + +# Run examples +docker run --rm liblpm-csharp dotnet run --project /build/bindings/csharp/LibLpm.Examples + +# Create NuGet package +docker run --rm -v "$PWD/packages:/packages" liblpm-csharp \ + bash -c "cd /build/bindings/csharp && dotnet pack -o /packages" +``` + ### liblpm-go Go bindings with cgo support. @@ -248,6 +283,7 @@ Approximate sizes (uncompressed): | liblpm-test | ~900MB | | liblpm-fuzz | ~1GB | | liblpm-cpp | ~800MB | +| liblpm-csharp | ~700MB | | liblpm-go | ~600MB | | liblpm-benchmark | ~1.5GB | | liblpm-deb | ~400MB | diff --git a/scripts/docker-build.sh b/scripts/docker-build.sh index 80f9949..dd0b641 100755 --- a/scripts/docker-build.sh +++ b/scripts/docker-build.sh @@ -50,6 +50,7 @@ Available Images: test - Testing environment fuzz - AFL++ fuzzing environment cpp - C++ bindings + csharp - C# bindings (.NET) go - Go bindings benchmark - DPDK benchmark environment all - Build all images (default) @@ -114,7 +115,7 @@ while [[ $# -gt 0 ]]; do VERBOSE="--progress=plain" shift ;; - base|dev|test|fuzz|cpp|go|benchmark|all) + base|dev|test|fuzz|cpp|csharp|go|benchmark|all) IMAGES+=("$1") shift ;; @@ -214,6 +215,9 @@ build_images() { cpp) build_image "cpp" "${DOCKER_DIR}/Dockerfile.cpp" ;; + csharp) + build_image "csharp" "${DOCKER_DIR}/Dockerfile.csharp" + ;; go) build_image "go" "${DOCKER_DIR}/Dockerfile.go" ;; @@ -226,6 +230,7 @@ build_images() { build_image "test" "${DOCKER_DIR}/Dockerfile.test" build_image "fuzz" "${DOCKER_DIR}/Dockerfile.fuzz" build_image "cpp" "${DOCKER_DIR}/Dockerfile.cpp" + build_image "csharp" "${DOCKER_DIR}/Dockerfile.csharp" build_image "go" "${DOCKER_DIR}/Dockerfile.go" build_image "benchmark" "${DOCKER_DIR}/Dockerfile.benchmark" ;;