[.NET] Scaffold GetString wrapper methods in LibCantera

Allows removal of boilerplate code and calls to GetString helper (which is now gone). Bonus: no more closure allocations when calling GetString and needing to capture extra variable into the FillStringBufferFunc, which only takes two arguments.
This commit is contained in:
Sammo Gabay
2025-06-17 07:20:41 -06:00
committed by Ingmar Schoegl
parent 9b7dda76be
commit 8d6876b167
11 changed files with 111 additions and 123 deletions
@@ -1,4 +1,5 @@
using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.Marshalling;
using Cantera.Interop;
using Xunit;
@@ -7,24 +8,28 @@ namespace Cantera.Tests;
public static class SourceGenerationTests
{
private static IReadOnlyList<MethodInfo> s_interopMethods =
typeof(LibCantera).GetMethods(
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)
.Where(m => m.IsDefined(typeof(LibraryImportAttribute)))
.ToList();
/// <remarks>
/// Get name rather than object for better compatibility with test runners
/// and IDE displays which may struggle with non-primitive theory data.
/// </remarks>
public static TheoryData<string> InteropMethodNames =>
new(typeof(LibCantera).GetMethods(
BindingFlags.Public | BindingFlags.Static)
// get name rather than object for better compatibility
// with test runners and IDE displays which may struggle with
// non-primitive theory data
.Select(m => m.Name));
new(s_interopMethods.Select(m => m.Name));
[Theory]
[MemberData(nameof(InteropMethodNames))]
public static void LibCantera_PInvokeReturnCodesChecked(string methodName)
{
var method = typeof(LibCantera).GetMethod(methodName);
Assert.NotNull(method);
var method = s_interopMethods.Single(m => m.Name == methodName);
var ReturnCodeCheckerType = typeof(LibCantera).GetNestedType(
var returnCodeCheckerType = typeof(LibCantera).GetNestedType(
"ReturnCodeChecker", BindingFlags.NonPublic);
Assert.NotNull(ReturnCodeCheckerType);
Assert.NotNull(returnCodeCheckerType);
var returnType = method.ReturnType;
var returnMarshaller = method.ReturnParameter
@@ -32,8 +37,8 @@ public static class SourceGenerationTests
// XOR - only one of these should be true
Assert.True(returnType.IsSubclassOf(typeof(CanteraHandle))
^ returnMarshaller == ReturnCodeCheckerType
^ returnMarshaller == returnCodeCheckerType
// del functions should be unchecked
^ method.Name.EndsWith("_del", StringComparison.Ordinal));
}
}
}
+2 -2
View File
@@ -67,10 +67,10 @@ public static class Application
static readonly LibCantera.LogCallback s_invokeMessageLoggedDelegate;
static readonly Lazy<string> s_version =
new(() => InteropUtil.GetString(10, LibCantera.ct_version));
new(LibCantera.ct_version);
static readonly Lazy<string> s_gitCommit =
new(() => InteropUtil.GetString(10, LibCantera.ct_gitCommit));
new(LibCantera.ct_gitCommit);
static readonly Lazy<DataDirectoryCollection> s_dataDirectories =
new(() => new DataDirectoryCollection());
@@ -13,11 +13,9 @@ public class DataDirectoryCollection : IReadOnlyList<DirectoryInfo>
{
static IEnumerable<DirectoryInfo> GetDirs()
{
const char sep = ';';
const string sep = ";";
return InteropUtil
.GetString(500, (size, buffer) =>
LibCantera.ct_getDataDirectories(sep.ToString(), size, buffer))
return LibCantera.ct_getDataDirectories(sep)
.Split(sep)
.Select(d => new DirectoryInfo(d));
}
+2 -5
View File
@@ -14,11 +14,8 @@ public class CanteraException : ExternalException
{
private CanteraException(string message) : base(message) { }
internal static void ThrowLatest()
{
var errorMessage = InteropUtil.GetString(500, LibCantera.ct_getCanteraError);
throw new CanteraException(errorMessage);
}
internal static void ThrowLatest() =>
throw new CanteraException(LibCantera.ct_getCanteraError());
}
/// <summary>
@@ -1,10 +1,6 @@
// This file is part of Cantera. See License.txt in the top-level directory or
// at https://cantera.org/license.txt for license and copyright information.
using System.Buffers;
using System.Diagnostics.CodeAnalysis;
using System.Text;
namespace Cantera.Interop;
static class InteropUtil
@@ -24,14 +20,6 @@ static class InteropUtil
Span<double> buffer)
where THandle : CanteraHandle;
/// <summary>
/// Represents a function that fills a byte buffer representing a native string.
/// </summary>
/// <remarks>
/// The Cantera C API specifies the size as an int.
/// </remarks>
public delegate int FillStringBufferFunc(int size, Span<byte> buffer);
/// <summary>
/// Checks the return code of the call into the native Cantera library
/// and throws a <see cref="CanteraException"/> if necessary
@@ -61,8 +49,7 @@ static class InteropUtil
CanteraException.ThrowLatest();
}
// some functions return negative when they want more chars, others positive!
return Math.Abs(code);
return code;
}
/// <summary>
@@ -108,63 +95,6 @@ static class InteropUtil
fillBufferFunc(handle, span.Length, span);
}
[SuppressMessage("Reliability", "CA2014:NoStackallocInLoops",
Justification = "Loop is executed at most twice.")]
public static string GetString(int initialSize, FillStringBufferFunc func)
{
// take up to two tries
// 1) use the initial size
// if the initial size was large enough, return the string
// if the initial size was not large enough ...
// 2) try again with the needed size
// if the needed size was large enough, return the string
// otherwise, catastrophe, throw!
for (var i = 0; i < 2; i++)
{
int neededSize;
if (initialSize <= 120)
{
Span<byte> span = stackalloc byte[initialSize];
if (TryGetString(span, func, out var value, out neededSize))
{
return value;
}
}
else
{
using (MemoryPool<byte>.Shared.Rent(initialSize, out var span))
{
if (TryGetString(span, func, out var value, out neededSize))
{
return value;
}
}
}
initialSize = neededSize;
}
throw new InvalidOperationException(
"Could not retrieve a string value from Cantera!");
static bool TryGetString(Span<byte> span, FillStringBufferFunc func,
[NotNullWhen(true)] out string? value, out int neededSize)
{
var initialSize = span.Length;
neededSize = func(initialSize, span);
if (initialSize >= neededSize)
{
value = Encoding.UTF8.GetString(span[..(neededSize - 1)]); // trim null byte
return true;
}
value = null;
return false;
}
}
public static int GetInteropBool(bool value) =>
value ? InteropConsts.True : InteropConsts.False;
}
@@ -9,6 +9,10 @@ namespace Cantera.Interop;
static partial class LibCantera
{
const string LibFile = "cantera_shared";
const int BufferSize = 360;
static void ThrowOnBadString() =>
throw new InvalidOperationException("Could not retrieve a string value from Cantera!");
public delegate void LogCallback(LogLevel logLevel,
[MarshalAs(UnmanagedType.LPUTF8Str)] string category,
@@ -83,10 +83,7 @@ public class SpeciesCollection : IReadOnlyList<Species>
for (var i = 0; i < count; i++)
{
int getName(int length, Span<byte> buffer) => LibCantera
.thermo_speciesName(handle, i, length, buffer);
var name = InteropUtil.GetString(10, getName);
var name = LibCantera.thermo_speciesName(handle, i);
_species.Add(new
(
@@ -53,6 +53,11 @@ class CsFunc(Func):
"""True if this function returns a handle."""
return self.ret_type.endswith("Handle")
def gets_string(self) -> bool:
"""True if this function is used to get a string."""
return (len(self.arglist) >= 2
and self.arglist[-1].p_type == "Span<byte>")
class CSharpSourceGenerator(SourceGenerator):
"""The SourceGenerator for scaffolding C# files for the .NET interface"""
@@ -96,18 +101,12 @@ class CSharpSourceGenerator(SourceGenerator):
"unsupported signature!")
sys.exit(1)
if prop_type in ["int", "double"]:
template = _LOADER.from_string(self._templates["csharp-property-int-double"])
if prop_type in ["int", "double", "string"]:
template = _LOADER.from_string(self._templates["csharp-property"])
return template.render(
prop_type=prop_type, cs_name=cs_name,
getter=getter_name, setter=setter_name)
if prop_type == "string":
template = _LOADER.from_string(self._templates["csharp-property-string"])
return template.render(
cs_name=cs_name, p_type="string",
getter=getter_name, setter=setter_name)
# TODO: Add ability to scaffold properties the use arrays of doubles.
# This will require looking up the function that gets the size
# of the array.
@@ -192,14 +191,29 @@ class CSharpSourceGenerator(SourceGenerator):
self._out_dir.joinpath(file_name).write_text(contents, encoding="utf-8")
def _scaffold_interop(self, header_file: str, cs_funcs: list[CsFunc]) -> None:
template = _LOADER.from_string(self._templates["csharp-interop-func"])
pinvoke_template = _LOADER.from_string(self._templates["csharp-interop-func"])
function_list = [
template.render(has_string_param=func.has_string_param(),
declaration=func.declaration(),
check_return=(not func.is_handle_release_func
and not func.returns_handle()))
pinvoke_template.render(has_string_param=func.has_string_param(),
declaration=func.declaration(),
check_return=(not func.is_handle_release_func
and not func.returns_handle()),
public=not func.gets_string())
for func in cs_funcs]
# Add wrappers for functions that get strings.
def transform_to_getstring_func(func: CsFunc) -> CsFunc:
arglist = ArgList(func.arglist[:-2])
return CsFunc('string', func.name, arglist, False, None)
getstring_template = _LOADER.from_string(self._templates["csharp-getstring-func"])
function_list += (
getstring_template.render(declaration=(transform_to_getstring_func(func)
.declaration()),
invocation=func.invocation(),
length_param_name=func.arglist[-2].name,
span_param_name=func.arglist[-1].name)
for func in cs_funcs if func.gets_string())
file_name = f"Interop.LibCantera.{header_file}.g.cs"
self._write_file(
file_name, "template_interop.g.cs.in", cs_functions=function_list)
@@ -11,8 +11,11 @@
// This file is part of Cantera. See License.txt in the top-level directory or
// at https://cantera.org/license.txt for license and copyright information.
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.Marshalling;
using System.Text;
namespace Cantera.Interop;
@@ -8,7 +8,48 @@ csharp-interop-func: |-
{%- if check_return %}
[return: MarshalUsing(typeof(ReturnCodeChecker))]
{%- endif %}
public static partial {{ declaration }};
{% if public %}public {% else %}private {% endif %}static partial {{ declaration }};
csharp-getstring-func: |-
[SkipLocalsInit]
public static {{ declaration }}
{
// try with stack-allocated buffer
Span<byte> {{ span_param_name }} = stackalloc byte[BufferSize];
var {{ length_param_name }} = BufferSize;
var neededSize = {{ invocation }};
if (neededSize <= {{ length_param_name }})
{
// remove null terminator byte
{{ span_param_name }} = {{ span_param_name }}[..(neededSize - 1)];
return Encoding.UTF8.GetString({{ span_param_name }});
}
// try with rented array buffer
// array will be at least neededSize but could be larger
var array = ArrayPool<byte>.Shared.Rent(neededSize);
{{ span_param_name }} = array;
{{ length_param_name }} = array.Length;
try
{
neededSize = {{ invocation }};
if (neededSize <= {{ length_param_name }})
{
// remove null terminator byte
{{ span_param_name }} = {{ span_param_name }}[..(neededSize - 1)];
// use span overload, which skips bounds checks
return Encoding.UTF8.GetString({{ span_param_name }});
}
}
finally
{
ArrayPool<byte>.Shared.Return(array);
}
ThrowOnBadString();
return null; // not reached
}
csharp-base-handle: |-
[NativeMarshalling(typeof(Marshaller<{{ class_name }}>))]
@@ -22,7 +63,7 @@ csharp-derived-handle: |-
[NativeMarshalling(typeof(Marshaller<{{ derived_class_name }}>))]
class {{ derived_class_name }} : {{ base_class_name }} { }
csharp-property-int-double: |-
csharp-property: |-
public {{ prop_type }} {{ cs_name }}
{
get => LibCantera.{{ getter }}(_handle);
@@ -30,13 +71,3 @@ csharp-property-int-double: |-
set => LibCantera.{{ setter }}(_handle, value);
{%- endif %}
}
csharp-property-string: |-
public string {{ cs_name }}
{
get => InteropUtil.GetString(40, (length, buffer) =>
LibCantera.{{ getter }}(_handle, length, buffer));
{% if setter -%}
set => LibCantera.{{ setter }}(_handle, value);
{%- endif %}
}
@@ -148,6 +148,11 @@ class ArgList:
def __iter__(self) -> Iterator[Param]:
return iter(self.params)
def param_names_str(self) -> str:
"""String representation of the argument list parameter names."""
args = ", ".join(par.name for par in self.params)
return f"({args})"
def short_str(self) -> str:
"""String representation of the argument list without parameter names."""
args = ", ".join(par.short_str() for par in self.params)
@@ -194,6 +199,10 @@ class Func:
r_type, name = name.rsplit(" ", 1)
return cls(r_type, name, arglist, brief, None, "", "", [])
def invocation(self) -> str:
"""Return a string representation of calling the function in an expression."""
return (f"{self.name}{self.arglist.param_names_str()}")
def declaration(self) -> str:
"""Return a string representation of the function without semicolon."""
return (f"{self.ret_type} {self.name}{self.arglist.long_str()}").strip()