Skip to content

Commit

Permalink
[.NET] Generate C# interop code automatically
Browse files Browse the repository at this point in the history
Uses an MSBuild task and a Python script to generate C# P/Invoke calls and SafeHandles from the Cantera CLib header files.
The script is written in Python because it's already part of the build environment.
  • Loading branch information
burkenyo authored and speth committed Aug 17, 2022
1 parent 37459a8 commit a0e7fac
Show file tree
Hide file tree
Showing 9 changed files with 306 additions and 51 deletions.
13 changes: 13 additions & 0 deletions interfaces/dotnet/Cantera/Cantera.csproj
Expand Up @@ -18,4 +18,17 @@
<PackageReference Include="System.Memory" Version="4.5.4" />
</ItemGroup>

<Target Name="GenerateInterop" BeforeTargets="CoreCompile">
<PropertyGroup>
<IncludePath>$([MSBuild]::NormalizePath(../../../include/cantera/clib))</IncludePath>
<GeneratedPath>$([MSBuild]::NormalizePath($(IntermediateOutputPath)))</GeneratedPath>
</PropertyGroup>

<Exec Command="python3 generate_interop.py $(IncludePath) $(GeneratedPath)"/>

<ItemGroup>
<Compile Remove="$(GeneratedPath)Interop.*.cs" />
<Compile Include="$(GeneratedPath)Interop.*.cs" />
</ItemGroup>
</Target>
</Project>
176 changes: 176 additions & 0 deletions interfaces/dotnet/Cantera/generate_interop.py
@@ -0,0 +1,176 @@
from operator import indexOf
import os
import re
import sys

import ruamel.yaml

print('Gnenerating interop types...')

prolog = ' [DllImport(LibFile)]\n public static extern'

type_map = {
'const char*': 'string',
'size_t': 'nuint',
'char*': 'byte*'
}


def join_params(params):
return ', '.join((param_name + ' ' + param_type for (param_name, param_type) in params))


def get_function_text(function):
(ref_type, name, params, _) = function
is_unsafe = any((param_type.endswith('*') for (param_type, _) in params))
if is_unsafe:
return f'{prolog} unsafe {ref_type} {name}({join_params(params)});'
else:
return f'{prolog} {ref_type} {name}({join_params(params)});'


def get_base_handle_text(handle):
(name, del_clazz) = handle

handle = f'''class {del_clazz} : CanteraHandle
{{
protected override bool ReleaseHandle() =>
Convert.ToBoolean(LibCantera.{name}(Value));
}}'''

return handle


def get_derived_handle_text(derived):
(derived, base) = derived

derived = f'''class {derived} : {base} {{ }}'''

return derived


def split_param(param_string):
parts = param_string.strip().rsplit(' ', 1)
if len(parts) == 2:
return tuple(parts)


def parse(c_function):
lparen = c_function.index('(')
rparen = c_function.index(')')
front = (c_function[0:lparen]).split()

params = (split_param(p) for p in c_function[lparen+1:rparen].split(','))
params = [s for s in params if s]

ret_type = front[-2]
name = front[-1]
return (ret_type, name, params)


def convert(parsed):
(ret_type, name, params) = parsed
(clazz, method) = tuple(name.split('_', 1))

del_clazz = None

if clazz != 'ct':
handle_clazz = clazz.capitalize() + 'Handle'

# It’s not a “global” function, therefore:
# * It wraps a constructor and returns a handle, or
# * It wraps an instance method and takes the handle as the first parameter.
if method.startswith('del'):
del_clazz = handle_clazz
elif method.startswith('new'):
ret_type = handle_clazz
else:
(_, param_name) = params[0]
params[0] = (handle_clazz, param_name)

for (c_type, cs_type) in type_map.items():
if (ret_type == c_type):
ret_type = cs_type
break

for i in range(0, len(params)):
(param_type, param_name) = params[i]

for (c_type, cs_type) in type_map.items():
if (param_type == c_type):
param_type = cs_type
break

if param_type.startswith('const '):
param_type = param_type.rsplit(' ', 1)[-1]

params[i] = (param_type, param_name)

return (ret_type, name, params, del_clazz)


def generate_interop(incl_file, ignore):
print(' ' + incl_file.path)

with open(incl_file.path, 'r') as f:
ct = f.read()

matches = re.finditer(r'CANTERA_CAPI.*?;', ct, re.DOTALL)
c_functions = [re.sub(r'\s+', ' ', m.group()) for m in matches]

if not c_functions:
return

parsed = (parse(f) for f in c_functions)

if ignore:
print(f' ignoring ' + str(ignore))

functions = [convert((ret_type, name, params)) for (ret_type, name, params) in parsed if name not in ignore]

functions_text = '\n\n'.join((get_function_text(f) for f in functions))

interop_text = f'''using System.Runtime.InteropServices;
namespace Cantera.Interop;
static partial class LibCantera
{{
{functions_text}
}}'''

with open(gen_file_dir + 'Interop.LibCantera.' + incl_file.name + '.g.cs', 'w') as f:
f.write(interop_text)

handles = [(name, del_clazz) for (_, name, _, del_clazz) in functions if del_clazz]

if not handles:
return

handles_text = 'namespace Cantera.Interop;\n\n' + '\n\n'.join((get_base_handle_text(h) for h in handles))

with open(gen_file_dir + 'Interop.Handles.' + incl_file.name + '.g.cs', 'w') as f:
f.write(handles_text)


(_, include_dir, gen_file_dir) = sys.argv

with open('generate_interop.yaml', 'r') as config_file:
config = ruamel.yaml.safe_load(config_file)

ignore = config['ignore']

for incl_file in os.scandir('Include/clib'):
if incl_file.name not in ignore or ignore[incl_file.name]:
generate_interop(incl_file, ignore.get(incl_file.name, []))

derived_handles = '\n\n'.join((get_derived_handle_text(d) for d in config['derived_handles'].items()))

derived_handles_text = f'''using System.Runtime.InteropServices;
namespace Cantera.Interop;
{derived_handles}'''

with open(gen_file_dir + 'Interop.Handles.g.cs', 'w') as f:
f.write(derived_handles_text)
20 changes: 20 additions & 0 deletions interfaces/dotnet/Cantera/generate_interop.yaml
@@ -0,0 +1,20 @@
# How ignore works:
# Put the name of a C header file with
# - an empty list to ignore the entire file
# - a list of function names to include the file but ignore those functions
ignore:
clib_defs.h: []
ctfunc.h: []
ctmatlab.h: []
ctonedim.h: []
ctreactor.h:
- flowReactor_setMassFlowRate
ctrpath.h: []

# Handles for which there is no special delete function,
# so we need to generate them manually because we can't
# discover the type name from the delete.
# Declare these as
# Derived: Base
derived_handles:
SurfHandle: ThermoHandle
11 changes: 3 additions & 8 deletions interfaces/dotnet/Cantera/src/CanteraException.cs
@@ -1,4 +1,5 @@
using System.Runtime.InteropServices;
using Cantera.Interop;

namespace Cantera;

Expand All @@ -7,17 +8,11 @@ namespace Cantera;
/// </summary>
public class CanteraException : ExternalException
{
static class LibCantera
{
[DllImport(Interop.LIBCANTERA)]
unsafe public static extern int ct_getCanteraError(int buflen, byte* buf);
}

private CanteraException(string message) : base(message) { }

unsafe internal static void ThrowLatest()
{
var errorMessage = Interop.GetString(500, LibCantera.ct_getCanteraError);
var errorMessage = InteropUtil.GetString(500, LibCantera.ct_getCanteraError);
throw new CanteraException(errorMessage);
}
}
}
29 changes: 0 additions & 29 deletions interfaces/dotnet/Cantera/src/CanteraHandle.cs

This file was deleted.

55 changes: 47 additions & 8 deletions interfaces/dotnet/Cantera/src/CanteraInfo.cs
@@ -1,28 +1,67 @@
using System.Runtime.InteropServices;
using System.Collections;
using Cantera.Interop;

namespace Cantera;

public static class CanteraInfo
{
static class LibCantera
public class DataDirectoryCollection : IReadOnlyList<DirectoryInfo>
{
[DllImport(Interop.LIBCANTERA)]
unsafe public static extern int ct_getCanteraVersion(int buflen, byte* buf);
unsafe static IEnumerable<DirectoryInfo> GetDirs()
{
const char sep = ';';

[DllImport(Interop.LIBCANTERA)]
unsafe public static extern int ct_getGitCommit(int buflen, byte* buf);
return InteropUtil
.GetString(500, (length, buffer) =>
LibCantera.ct_getDataDirectories(length, buffer, sep.ToString()))
.Split(sep)
.Select(d => new DirectoryInfo(d));
}

readonly List<DirectoryInfo> _dirs;

public DirectoryInfo this[int index] => _dirs[index];

public int Count => _dirs.Count;

internal DataDirectoryCollection()
{
_dirs = GetDirs().ToList();
}

public void Add(string dir) =>
Add(new DirectoryInfo(dir));

public void Add(DirectoryInfo dir)
{
InteropUtil.CheckReturn(LibCantera.ct_addCanteraDirectory((nuint) dir.FullName.Length, dir.FullName));

_dirs.Clear();
_dirs.AddRange(GetDirs());
}

public IEnumerator<DirectoryInfo> GetEnumerator() =>
_dirs.GetEnumerator();

IEnumerator IEnumerable.GetEnumerator() =>
_dirs.GetEnumerator();
}

unsafe static readonly Lazy<string> _version =
new Lazy<string>(() => Interop.GetString(10, LibCantera.ct_getCanteraVersion));
new Lazy<string>(() => InteropUtil.GetString(10, LibCantera.ct_getCanteraVersion));

unsafe static readonly Lazy<string> _gitCommit =
new Lazy<string>(() => Interop.GetString(10, LibCantera.ct_getGitCommit));
new Lazy<string>(() => InteropUtil.GetString(10, LibCantera.ct_getGitCommit));

unsafe static readonly Lazy<DataDirectoryCollection> _dataDirectories =
new Lazy<DataDirectoryCollection>(() => new DataDirectoryCollection());

public static string Version =>
_version.Value;

public static string GitCommit =>
_gitCommit.Value;

unsafe public static DataDirectoryCollection DataDirectories =>
_dataDirectories.Value;
}
37 changes: 37 additions & 0 deletions interfaces/dotnet/Cantera/src/Interop/CanteraHandle.cs
@@ -0,0 +1,37 @@
using System.Runtime.InteropServices;

namespace Cantera.Interop;

/// <summary>
/// The base class for a handle to a Cantera object.
/// </summary>
/// </remarks>
/// We use the SafeHandle class, which has low-level support in the runtime to ensure
/// proper reference counting and cleanup and is thread-safe. This allows us to use
/// the dispose pattern easily and safely and without having to write our own finalizer.
/// Cantera uses signed 32-bit ints for handles, yet SafeHandle uses "native int" IntPtr.
/// The Value and IsValid properties are designed to account for this and only consider
/// the lower 32-bit of the IntPtr on 64-bit systems.
/// <remarks>
abstract class CanteraHandle : SafeHandle
{
static IntPtr Invalid = IntPtr.Size == 4
? new IntPtr(-1) // 32-bit IntPtr: 0xFFFFFFFF
: new IntPtr((long) unchecked((uint) -1)); // 64-bit IntPtr: 0x00000000FFFFFFFF

public CanteraHandle() : base(Invalid, true) { }

protected int Value =>
IntPtr.Size == 4
? (int) handle
: (int)(long) handle; // removes any leading bits

public override bool IsInvalid =>
Value < 0;

public void EnsureValid()
{
if (IsInvalid)
CanteraException.ThrowLatest();
}
}

0 comments on commit a0e7fac

Please sign in to comment.