Skip to content

Commit a39cbac

Browse files
committed
generate enums for flags and fix code comment extraction for function args
1 parent 51a7d98 commit a39cbac

File tree

10 files changed

+837
-660
lines changed

10 files changed

+837
-660
lines changed

Secp256k1.Net.InteropGen/HeaderParser.cs

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -785,18 +785,47 @@ private List<string> SplitParameters(string paramsStr)
785785
return null;
786786

787787
// Look for param in Args:, In:, Out:, or In/Out: sections
788-
var patterns = new[]
788+
// The description ends when we hit:
789+
// - Another section marker (Args:, In:, Out:, In/Out:, Returns:)
790+
// - Another parameter name pattern (word followed by colon at start of description area)
791+
// - End of comment
792+
793+
// Pattern to match the start of the parameter description
794+
var startPatterns = new[]
789795
{
790-
$@"\*\s*(?:Args|In|Out|In/Out):\s*{Regex.Escape(paramName)}:\s*([^\n]+(?:\n\s*\*\s+[^\n]+)*)",
791-
$@"\*\s+{Regex.Escape(paramName)}:\s*([^\n]+)"
796+
$@"\*\s*(?:Args|In|Out|In/Out):\s*{Regex.Escape(paramName)}:\s*",
797+
$@"\*\s+{Regex.Escape(paramName)}:\s*"
792798
};
793799

794-
foreach (var pattern in patterns)
800+
foreach (var startPattern in startPatterns)
795801
{
796-
var match = Regex.Match(docComment, pattern, RegexOptions.IgnoreCase);
797-
if (match.Success)
802+
var startMatch = Regex.Match(docComment, startPattern, RegexOptions.IgnoreCase);
803+
if (startMatch.Success)
798804
{
799-
return CleanMultilineText(match.Groups[1].Value);
805+
// Find where description starts
806+
var descStart = startMatch.Index + startMatch.Length;
807+
var remaining = docComment.Substring(descStart);
808+
809+
// Find where description ends - look for next parameter or section marker
810+
// Pattern: newline, optional whitespace, *, optional whitespace, then either:
811+
// - A section marker like "In:", "Out:", "In/Out:", "Args:", "Returns:"
812+
// - A parameter name pattern: "word:" at the start of the content area
813+
var endPattern = @"\n\s*\*\s*(?:(?:Args|In|Out|In/Out|Returns):|\s*\w+:\s)";
814+
var endMatch = Regex.Match(remaining, endPattern);
815+
816+
string description;
817+
if (endMatch.Success)
818+
{
819+
description = remaining.Substring(0, endMatch.Index);
820+
}
821+
else
822+
{
823+
// No next param found, take until end of comment (but stop at */)
824+
var commentEnd = remaining.IndexOf("*/");
825+
description = commentEnd >= 0 ? remaining.Substring(0, commentEnd) : remaining;
826+
}
827+
828+
return CleanMultilineText(description);
800829
}
801830
}
802831

Secp256k1.Net.InteropGen/InteropGenerator.cs

Lines changed: 195 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,181 @@ private static string FormatXmlDescription(string? text)
569569
return result.ToString();
570570
}
571571

572+
#region Enum Generation
573+
574+
/// <summary>
575+
/// Defines enum groupings for constants. Maps enum name to the list of constant names to include.
576+
/// </summary>
577+
private static readonly Dictionary<string, EnumDefinition> EnumDefinitions = new()
578+
{
579+
["Secp256k1EcFlags"] = new EnumDefinition
580+
{
581+
Description = "Flags for public key serialization format.",
582+
IsFlags = false,
583+
Members = new()
584+
{
585+
{ "SECP256K1_EC_COMPRESSED", "Compressed format (33 bytes)." },
586+
{ "SECP256K1_EC_UNCOMPRESSED", "Uncompressed format (65 bytes)." },
587+
}
588+
},
589+
["Secp256k1ContextFlags"] = new EnumDefinition
590+
{
591+
Description = "Flags for secp256k1 context creation.",
592+
IsFlags = false,
593+
Members = new()
594+
{
595+
{ "SECP256K1_CONTEXT_NONE", "Creates a context sufficient for all functionality." },
596+
}
597+
},
598+
};
599+
600+
/// <summary>
601+
/// Maps function parameters (by function name + param name) to the enum type they should use.
602+
/// </summary>
603+
private static readonly Dictionary<(string FunctionName, string ParamName), string> ParameterEnumMappings = new()
604+
{
605+
{ ("secp256k1_ec_pubkey_serialize", "flags"), "Secp256k1EcFlags" },
606+
};
607+
608+
private class EnumDefinition
609+
{
610+
public string? Description { get; set; }
611+
public bool IsFlags { get; set; }
612+
public Dictionary<string, string?> Members { get; set; } = new();
613+
}
614+
615+
/// <summary>
616+
/// Generates enum types from constants based on predefined groupings.
617+
/// </summary>
618+
public void GenerateEnums(StringBuilder sb, Secp256k1Api api)
619+
{
620+
foreach (var (enumName, enumDef) in EnumDefinitions)
621+
{
622+
sb.AppendLine();
623+
if (!string.IsNullOrEmpty(enumDef.Description))
624+
{
625+
sb.AppendLine($" /// <summary>{enumDef.Description}</summary>");
626+
}
627+
if (enumDef.IsFlags)
628+
{
629+
sb.AppendLine(" [Flags]");
630+
}
631+
sb.AppendLine($" public enum {enumName} : uint");
632+
sb.AppendLine(" {");
633+
634+
foreach (var (constantName, memberDesc) in enumDef.Members)
635+
{
636+
var constant = api.Constants.FirstOrDefault(c => c.Name == constantName);
637+
if (constant == null) continue;
638+
639+
// Generate member name by removing SECP256K1_ prefix and converting to PascalCase
640+
var memberName = GetEnumMemberName(constantName);
641+
642+
// Use numeric value if available, otherwise try to evaluate the expression
643+
var value = constant.NumericValue?.ToString() ?? EvaluateConstantValue(constant.Value, api);
644+
645+
var desc = memberDesc ?? constant.Description;
646+
if (!string.IsNullOrEmpty(desc))
647+
{
648+
var cleanDesc = CleanDescription(desc);
649+
sb.AppendLine($" /// <summary>{EscapeXml(cleanDesc)}</summary>");
650+
}
651+
sb.AppendLine($" {memberName} = {value},");
652+
}
653+
654+
sb.AppendLine(" }");
655+
}
656+
}
657+
658+
/// <summary>
659+
/// Converts a constant name like SECP256K1_EC_COMPRESSED to a C# enum member name like Compressed.
660+
/// </summary>
661+
private static string GetEnumMemberName(string constantName)
662+
{
663+
// Remove SECP256K1_ prefix
664+
var name = constantName;
665+
if (name.StartsWith("SECP256K1_"))
666+
name = name.Substring("SECP256K1_".Length);
667+
668+
// Remove EC_ prefix for EC flags
669+
if (name.StartsWith("EC_"))
670+
name = name.Substring("EC_".Length);
671+
672+
// Remove CONTEXT_ prefix for context flags
673+
if (name.StartsWith("CONTEXT_"))
674+
name = name.Substring("CONTEXT_".Length);
675+
676+
// Convert SCREAMING_SNAKE_CASE to PascalCase
677+
var parts = name.Split('_');
678+
return string.Join("", parts.Select(p =>
679+
p.Length > 0 ? char.ToUpper(p[0]) + p.Substring(1).ToLower() : ""));
680+
}
681+
682+
/// <summary>
683+
/// Evaluates a constant value expression that may reference other constants.
684+
/// </summary>
685+
private static string EvaluateConstantValue(string value, Secp256k1Api api)
686+
{
687+
// Handle simple numeric values
688+
if (int.TryParse(value, out var intVal))
689+
return intVal.ToString();
690+
if (value.StartsWith("0x") && int.TryParse(value.Substring(2), System.Globalization.NumberStyles.HexNumber, null, out intVal))
691+
return intVal.ToString();
692+
693+
// Handle bit shifts like (1 << 8)
694+
var shiftMatch = System.Text.RegularExpressions.Regex.Match(value, @"\((\d+)\s*<<\s*(\d+)\)");
695+
if (shiftMatch.Success)
696+
{
697+
var baseVal = int.Parse(shiftMatch.Groups[1].Value);
698+
var shift = int.Parse(shiftMatch.Groups[2].Value);
699+
return (baseVal << shift).ToString();
700+
}
701+
702+
// Handle expressions that reference other constants like (SECP256K1_FLAGS_TYPE_COMPRESSION | SECP256K1_FLAGS_BIT_COMPRESSION)
703+
var orMatch = System.Text.RegularExpressions.Regex.Match(value, @"\((\w+)\s*\|\s*(\w+)\)");
704+
if (orMatch.Success)
705+
{
706+
var left = ResolveConstantValue(orMatch.Groups[1].Value, api);
707+
var right = ResolveConstantValue(orMatch.Groups[2].Value, api);
708+
if (left.HasValue && right.HasValue)
709+
return (left.Value | right.Value).ToString();
710+
}
711+
712+
// Handle single constant reference like (SECP256K1_FLAGS_TYPE_COMPRESSION)
713+
var singleMatch = System.Text.RegularExpressions.Regex.Match(value, @"\((\w+)\)");
714+
if (singleMatch.Success)
715+
{
716+
var resolved = ResolveConstantValue(singleMatch.Groups[1].Value, api);
717+
if (resolved.HasValue)
718+
return resolved.Value.ToString();
719+
}
720+
721+
// Fallback - return as-is (will likely cause compile error if invalid)
722+
return value;
723+
}
724+
725+
/// <summary>
726+
/// Resolves a constant name to its numeric value.
727+
/// </summary>
728+
private static long? ResolveConstantValue(string constantName, Secp256k1Api api)
729+
{
730+
var constant = api.Constants.FirstOrDefault(c => c.Name == constantName);
731+
if (constant == null)
732+
return null;
733+
734+
if (constant.NumericValue.HasValue)
735+
return constant.NumericValue.Value;
736+
737+
// Try to evaluate the expression recursively
738+
var evaluated = EvaluateConstantValue(constant.Value, api);
739+
if (long.TryParse(evaluated, out var result))
740+
return result;
741+
742+
return null;
743+
}
744+
745+
#endregion
746+
572747
#region Wrapper Generation
573748

574749
// Functions to skip in wrapper generation (need manual implementation or are internal)
@@ -646,6 +821,9 @@ public string GenerateWrappers(Secp256k1Api api)
646821
sb.AppendLine("namespace Secp256k1Net");
647822
sb.AppendLine("{");
648823

824+
// Generate enum types from constants
825+
GenerateEnums(sb, api);
826+
649827
// Generate user-friendly delegate types for callback functions
650828
GenerateUserFriendlyCallbackDelegates(sb, api);
651829

@@ -1135,7 +1313,7 @@ private List<WrapperParameter> GetWrapperParameters(FunctionDef func, Dictionary
11351313
};
11361314

11371315
// Determine wrapper type and size
1138-
DetermineWrapperType(wrapper, param, structSizes);
1316+
DetermineWrapperType(wrapper, param, structSizes, func.Name);
11391317

11401318
result.Add(wrapper);
11411319
}
@@ -1158,7 +1336,7 @@ private List<WrapperParameter> GetWrapperParameters(FunctionDef func, Dictionary
11581336
return result;
11591337
}
11601338

1161-
private void DetermineWrapperType(WrapperParameter wrapper, ParameterDef param, Dictionary<string, int> structSizes)
1339+
private void DetermineWrapperType(WrapperParameter wrapper, ParameterDef param, Dictionary<string, int> structSizes, string functionName)
11621340
{
11631341
var cType = param.Type.Trim();
11641342
var name = param.Name;
@@ -1214,6 +1392,15 @@ private void DetermineWrapperType(WrapperParameter wrapper, ParameterDef param,
12141392
return;
12151393
}
12161394

1395+
// Check for enum mappings for this parameter
1396+
if (ParameterEnumMappings.TryGetValue((functionName, name), out var enumType))
1397+
{
1398+
wrapper.WrapperType = enumType;
1399+
wrapper.WrapperName = SanitizeParamName(name);
1400+
wrapper.IsEnumParam = true;
1401+
return;
1402+
}
1403+
12171404
// Non-pointer primitive types
12181405
if (!cType.Contains("*"))
12191406
{
@@ -1336,6 +1523,11 @@ private static string BuildNativeCallArgs(FunctionDef func, List<WrapperParamete
13361523
var cleanName = param.WrapperName.TrimStart('@');
13371524
args.Add($"{cleanName}Ptr");
13381525
}
1526+
else if (param.IsEnumParam)
1527+
{
1528+
// Cast enum to uint for native call
1529+
args.Add($"(uint){param.WrapperName}");
1530+
}
13391531
else
13401532
{
13411533
args.Add(param.WrapperName);
@@ -1364,6 +1556,7 @@ private class WrapperParameter
13641556
public bool IsOptionalCallback { get; set; } // Optional callback/data parameter - pass IntPtr.Zero
13651557
public string? IsLengthFor { get; set; } // If this is a length param, the name of the buffer param it's for
13661558
public string? LengthForSpanName { get; set; } // The wrapper name of the span this length is for (resolved)
1559+
public bool IsEnumParam { get; set; } // If true, this param uses an enum type and needs casting to uint
13671560
}
13681561

13691562
/// <summary>

Secp256k1.Net.Test/GeneratedWrapperTests.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public void EcPubkeyParse_ValidCompressedKey_Succeeds()
7373

7474
var serialized = new byte[33];
7575
nuint outputLen = 33;
76-
Assert.IsTrue(secp256k1.EcPubkeySerialize(serialized, ref outputLen, pubkey, (uint)Flags.SECP256K1_EC_COMPRESSED));
76+
Assert.IsTrue(secp256k1.EcPubkeySerialize(serialized, ref outputLen, pubkey, Secp256k1EcFlags.Compressed));
7777

7878
// Parse the compressed key
7979
var parsedPubkey = new byte[64];
@@ -91,7 +91,7 @@ public void EcPubkeyParse_ValidUncompressedKey_Succeeds()
9191

9292
var serialized = new byte[65];
9393
nuint outputLen = 65;
94-
Assert.IsTrue(secp256k1.EcPubkeySerialize(serialized, ref outputLen, pubkey, (uint)Flags.SECP256K1_EC_UNCOMPRESSED));
94+
Assert.IsTrue(secp256k1.EcPubkeySerialize(serialized, ref outputLen, pubkey, Secp256k1EcFlags.Uncompressed));
9595

9696
var parsedPubkey = new byte[64];
9797
Assert.IsTrue(secp256k1.EcPubkeyParse(parsedPubkey, serialized));
@@ -108,7 +108,7 @@ public void EcPubkeySerialize_Compressed_Succeeds()
108108

109109
var output = new byte[33];
110110
nuint outputLen = 33;
111-
Assert.IsTrue(secp256k1.EcPubkeySerialize(output, ref outputLen, pubkey, (uint)Flags.SECP256K1_EC_COMPRESSED));
111+
Assert.IsTrue(secp256k1.EcPubkeySerialize(output, ref outputLen, pubkey, Secp256k1EcFlags.Compressed));
112112
Assert.AreEqual((nuint)33, outputLen);
113113
// Compressed keys start with 0x02 or 0x03
114114
Assert.IsTrue(output[0] == 0x02 || output[0] == 0x03);
@@ -124,7 +124,7 @@ public void EcPubkeySerialize_Uncompressed_Succeeds()
124124

125125
var output = new byte[65];
126126
nuint outputLen = 65;
127-
Assert.IsTrue(secp256k1.EcPubkeySerialize(output, ref outputLen, pubkey, (uint)Flags.SECP256K1_EC_UNCOMPRESSED));
127+
Assert.IsTrue(secp256k1.EcPubkeySerialize(output, ref outputLen, pubkey, Secp256k1EcFlags.Uncompressed));
128128
Assert.AreEqual((nuint)65, outputLen);
129129
// Uncompressed keys start with 0x04
130130
Assert.AreEqual(0x04, output[0]);
@@ -869,8 +869,8 @@ public void EcPubkeySort_SortsTwoPubkeys()
869869
var serialized2 = new byte[33];
870870
nuint outputLen1 = 33;
871871
nuint outputLen2 = 33;
872-
Assert.IsTrue(secp256k1.EcPubkeySerialize(serialized1, ref outputLen1, pubkey1, (uint)Flags.SECP256K1_EC_COMPRESSED));
873-
Assert.IsTrue(secp256k1.EcPubkeySerialize(serialized2, ref outputLen2, pubkey2, (uint)Flags.SECP256K1_EC_COMPRESSED));
872+
Assert.IsTrue(secp256k1.EcPubkeySerialize(serialized1, ref outputLen1, pubkey1, Secp256k1EcFlags.Compressed));
873+
Assert.IsTrue(secp256k1.EcPubkeySerialize(serialized2, ref outputLen2, pubkey2, Secp256k1EcFlags.Compressed));
874874

875875
// Determine which should come first lexicographically
876876
var comparison = CompareBytes(serialized1, serialized2);
@@ -1047,7 +1047,7 @@ public void EcPubkeySerialize_TooSmallOutput_ReturnsFalse()
10471047
nuint outputLen = 32;
10481048

10491049
// The native library will fail and potentially write an error to stderr
1050-
var result = secp256k1.EcPubkeySerialize(output, ref outputLen, pubkey, (uint)Flags.SECP256K1_EC_COMPRESSED);
1050+
var result = secp256k1.EcPubkeySerialize(output, ref outputLen, pubkey, Secp256k1EcFlags.Compressed);
10511051
Assert.IsFalse(result);
10521052
}
10531053

0 commit comments

Comments
 (0)