Skip to content

Commit 1a74a7e

Browse files
committed
C#: Add generic commands
1 parent 99ca0a5 commit 1a74a7e

File tree

3 files changed

+264
-1
lines changed

3 files changed

+264
-1
lines changed

csharp/bindings_generator.py

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import argparse
2+
import json
3+
import os
4+
from os.path import join
5+
6+
RUST_CODE_PATH = './ffi/src/lib.rs'
7+
8+
# C# template for DllImport and RequestType enum entries
9+
C_SHARP_DllImport_TEMPLATE = """
10+
[DllImport("libglide_rs", CallingConvention = CallingConvention.Cdecl, EntryPoint = "{func_name}")]
11+
private static extern long {func_name}({params});
12+
"""
13+
14+
C_SHARP_REQUESTTYPE_TEMPLATE = """
15+
{enum_name} = {enum_value}, // {description}
16+
"""
17+
18+
def generate_bindings_and_enum(functions):
19+
"""
20+
This function generates C# bindings for Rust FFI functions and creates an enum for RequestType.
21+
It takes a list of function definitions, extracts the function names and parameters, and
22+
generates corresponding RequestType enum entries and DllImport statements for each Rust function.
23+
24+
Args:
25+
functions (list): A list of tuples containing function names, parameters, and return types.
26+
27+
Generates a C# file 'generated_bindings.cs' containing the RequestType enum and DllImport bindings.
28+
"""
29+
request_type_enum = []
30+
csharp_imports = []
31+
32+
for i, (func_name, params_str, return_type) in enumerate(functions, start=1500):
33+
# Generate the RequestType enum entry for each function
34+
enum_value = i
35+
description = f"Request Type for {func_name.upper()} command"
36+
enum_entry = C_SHARP_REQUESTTYPE_TEMPLATE.format(
37+
enum_name=func_name.capitalize(),
38+
enum_value=enum_value,
39+
description=description
40+
)
41+
request_type_enum.append(enum_entry)
42+
43+
# Generate the DllImport statement for each function
44+
params = ', '.join([f"IntPtr {param.strip()}" for param in params_str.split(',') if param.strip()])
45+
dll_import = C_SHARP_DllImport_TEMPLATE.format(
46+
func_name=func_name,
47+
params=params
48+
)
49+
csharp_imports.append(dll_import)
50+
51+
enum_code = "\n".join(request_type_enum)
52+
import_code = "\n".join(csharp_imports)
53+
54+
with open('generated_bindings.cs', 'w') as file:
55+
file.write(f"""
56+
// Generated C# Bindings for Rust FFI functions
57+
58+
namespace Valkey.Glide
59+
{{
60+
internal enum RequestType : uint
61+
{{
62+
{enum_code}
63+
}}
64+
65+
// FFI Bindings
66+
public static class FfiBindings
67+
{{
68+
{import_code}
69+
}}
70+
}}
71+
""")
72+
print("Bindings and enum generation complete!")
73+
74+
def main():
75+
"""
76+
Main function that parses the input JSON files containing command information,
77+
categorizes commands based on their routing policy, and generates FFI bindings
78+
for supported commands like COPY and DEL. It also prints categorized commands.
79+
"""
80+
parser = argparse.ArgumentParser(
81+
description="Analyzes command info json and categorizes commands into their RouteBy categories"
82+
)
83+
parser.add_argument(
84+
"--commands-dir",
85+
type=str,
86+
help="Path to the directory containing the command info json files (example: ../../valkey/src/commands)",
87+
required=True,
88+
)
89+
90+
args = parser.parse_args()
91+
commands_dir = args.commands_dir
92+
if not os.path.exists(commands_dir):
93+
raise parser.error(
94+
"The command info directory passed to the '--commands-dir' argument does not exist"
95+
)
96+
97+
# Categories for command routing
98+
all_nodes = CommandCategory("AllNodes", "Commands with an ALL_NODES request policy")
99+
all_primaries = CommandCategory(
100+
"AllPrimaries", "Commands with an ALL_SHARDS request policy"
101+
)
102+
uncategorized = CommandCategory(
103+
"Uncategorized",
104+
"Commands that don't fall into the other categories. These commands will have to be manually categorized.",
105+
)
106+
107+
categories = [
108+
all_nodes,
109+
all_primaries,
110+
uncategorized,
111+
]
112+
113+
print("Gathering command info...\n")
114+
115+
# This list will hold the Rust functions for which we want to generate C# bindings
116+
functions = []
117+
118+
# Process each command info json file in the provided directory
119+
for filename in os.listdir(commands_dir):
120+
file_path = join(commands_dir, filename)
121+
_, file_extension = os.path.splitext(file_path)
122+
if file_extension != ".json":
123+
print(f"Note: {filename} is not a json file and will thus be ignored")
124+
continue
125+
126+
file = open(file_path)
127+
command_json = json.load(file)
128+
if len(command_json) == 0:
129+
raise Exception(
130+
f"Command json for {filename} was empty. A json object with information about the command was expected."
131+
)
132+
133+
command_name = next(iter(command_json))
134+
command_info = command_json[command_name]
135+
136+
# If the command has a container (e.g., 'XINFO GROUPS'), handle it
137+
if "container" in command_info:
138+
command_name = f"{command_info['container']} {command_name}"
139+
140+
# Categorize commands based on their routing policy
141+
if "command_tips" in command_info:
142+
request_policy = get_request_policy(command_info["command_tips"])
143+
if request_policy == "ALL_NODES":
144+
all_nodes.add_command(command_name)
145+
elif request_policy == "ALL_SHARDS":
146+
all_primaries.add_command(command_name)
147+
148+
# If the command is recognized (e.g., COPY or DEL), add to functions list
149+
if command_name.lower() in ["copy", "del"]:
150+
functions.append((command_name, "source, destination", "i64"))
151+
152+
uncategorized.add_command(command_name)
153+
154+
generate_bindings_and_enum(functions)
155+
156+
print("\nCategorizing commands...")
157+
for category in categories:
158+
print_category(category)
159+
160+
def get_request_policy(command_tips):
161+
"""
162+
Extracts the request policy (e.g., ALL_NODES, ALL_SHARDS) from command tips.
163+
164+
Args:
165+
command_tips (list): A list of command tips that may contain a REQUEST_POLICY.
166+
167+
Returns:
168+
str: The request policy string if found, otherwise None.
169+
"""
170+
for command_tip in command_tips:
171+
if command_tip.startswith("REQUEST_POLICY:"):
172+
return command_tip[len("REQUEST_POLICY:") :]
173+
return None
174+
175+
def print_category(category):
176+
"""
177+
Prints the categorized commands along with their descriptions.
178+
"""
179+
print("============================")
180+
print(f"Category: {category.name} commands")
181+
print(f"Description: {category.description}")
182+
print("List of commands in this category:\n")
183+
184+
if len(category.commands) == 0:
185+
print("(No commands found for this category)")
186+
else:
187+
category.commands.sort()
188+
for command_name in category.commands:
189+
print(f"{command_name}")
190+
191+
print("\n")
192+
193+
# represent command categories
194+
class CommandCategory:
195+
def __init__(self, name, description):
196+
"""Initializes a new category for organizing commands."""
197+
self.name = name
198+
self.description = description
199+
self.commands = []
200+
201+
def add_command(self, command_name):
202+
"""Adds a command to the category."""
203+
self.commands.append(command_name)
204+
205+
if __name__ == "__main__":
206+
main()

csharp/sources/Valkey.Glide/Commands/IGenericCommands.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,4 +163,20 @@ public interface IGenericCommands
163163
/// <param name="options">A <see cref="BatchOptions" /> object containing execution options.</param>
164164
/// <returns>An array of results, where each entry corresponds to a command’s execution result.</returns>
165165
Task<object?[]?> Exec(Batch batch, bool raiseOnError, BatchOptions options);
166+
167+
/// <summary>
168+
/// Removes one or more keys. Returns the number of keys deleted.
169+
/// </summary>
170+
Task<long> Del(params GlideString[] keys);
171+
172+
/// <summary>
173+
/// Copies the value at the source key to the destination key.
174+
/// </summary>
175+
/// <param name="source">The source key</param>
176+
/// <param name="destination">The destination key</param>
177+
/// <param name="destinationDb">Optional DB index for destination</param>
178+
/// <param name="replace">If true, overwrite the destination if it exists</param>
179+
Task<long> Copy(GlideString source, GlideString destination, int? destinationDb = null, bool replace = false);
166180
}
181+
182+

csharp/sources/Valkey.Glide/GlideClient.cs

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0
1+
// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0
22

33
using Valkey.Glide.Commands;
44
using Valkey.Glide.Commands.Options;
@@ -75,4 +75,45 @@ public static async Task<GlideClient> CreateClient(StandaloneClientConfiguration
7575
public async Task<string> Info(InfoOptions.Section[] sections)
7676
=> await Command(RequestType.Info, sections.ToGlideStrings(), resp
7777
=> HandleServerResponse<GlideString, string>(resp, false, gs => gs.ToString()));
78+
=> await Command(RequestType.CustomCommand, args, resp => HandleServerResponse<object?>(resp, true));
79+
80+
public async Task<long> Del(params GlideString[] keys)
81+
{
82+
var args = new GlideString[keys.Length + 1];
83+
args[0] = "DEL";
84+
Array.Copy(keys, 0, args, 1, keys.Length);
85+
86+
return await Command(
87+
RequestType.CustomCommand,
88+
args,
89+
resp => HandleServerResponse<long>(resp, false)
90+
);
91+
}
92+
93+
public async Task<long> Copy(GlideString source, GlideString destination, int? destinationDb = null, bool replace = false)
94+
{
95+
var args = new List<GlideString>
96+
{
97+
"COPY",
98+
source,
99+
destination
100+
};
101+
102+
if (destinationDb.HasValue)
103+
{
104+
args.Add("DB");
105+
args.Add(destinationDb.Value.ToString());
106+
}
107+
108+
if (replace)
109+
{
110+
args.Add("REPLACE");
111+
}
112+
113+
return await Command(
114+
RequestType.CustomCommand,
115+
args.ToArray(),
116+
resp => HandleServerResponse<long>(resp, false)
117+
);
118+
}
78119
}

0 commit comments

Comments
 (0)