mirror of
https://github.com/Cantera/cantera.git
synced 2026-08-08 12:08:13 -05:00
[.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:
committed by
Ingmar Schoegl
parent
9b7dda76be
commit
8d6876b167
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user