Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interface for .NET languages: initial pass #1331

Merged
merged 45 commits into from Aug 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
2a4734d
[.NET] Stub out foundation classes for .Net interface
burkenyo May 26, 2022
6204208
[.NET] Add support for retrieving array-of-doubles properties via Lib…
burkenyo May 27, 2022
2591213
[.NET] Generate C# interop code automatically
burkenyo May 28, 2022
617cce3
[.NET] Move source generator into a pluggable python module
burkenyo May 28, 2022
f5f35b4
[.NET] Add appropriate dataclasses to sourcegen
burkenyo May 29, 2022
ff2e87e
[.NET] Stub out platform-sniffing with compilation switches
burkenyo Jun 3, 2022
e17b565
[.NET] Improve C# SourceGenerator readability
burkenyo Jun 3, 2022
ae02e1e
[.NET] Add a crosswalk to map Handle classes
burkenyo Jun 3, 2022
3319819
[.NET] Add test project and support for including native libs in build
burkenyo Jun 3, 2022
b2be835
[.NET] Add ability to scaffold higher-level C# classes from CLIB
burkenyo Jun 11, 2022
7d2c755
[.NET] First pass at a ThermoPhase class
burkenyo Jun 11, 2022
9ae8991
[.NET] Move test classes under src directory for consistency
burkenyo Jun 12, 2022
65672d6
[.NET] Rethink Species and SpeciesCollection
burkenyo Jun 15, 2022
a8da77b
[.NET] Add ability to plug into Cantera’s logging functionality
burkenyo Jun 25, 2022
0568f04
[.NET] Flush-out ThermoPhase class and add Cantera.Examples project
burkenyo Jun 15, 2022
a4ab35d
[.NET] Clean-up project files
burkenyo Jul 3, 2022
4a36a90
[.NET] Centralize static methods and object creation
burkenyo Jul 3, 2022
723592c
[.NET] Use case-insensitive comparison for Species and SpeciesCollection
burkenyo Jul 17, 2022
cb655c8
[.NET] Address review comments re: formatting and typos
burkenyo Jul 19, 2022
5a759d1
[.NET] [sourcegen] Write preambles in generated files
burkenyo Jul 20, 2022
0036957
[.NET] [sourcegen] Add readmes
burkenyo Jul 20, 2022
02ed8f0
[.NET] Optimize setting thermo pairs
burkenyo Jul 20, 2022
a6f6e4b
[.NET] Handle callback exceptions correctly
burkenyo Jul 24, 2022
b31e2b5
[.NET] rework examples
burkenyo Jul 19, 2022
1ceb74f
[sourcegen] simplify normalize_indent helper function
burkenyo Jul 25, 2022
0d0dcb2
[.NET] Update private field names and add XML doc comments
burkenyo Jul 26, 2022
181872c
[.NET] Optimize CallbackException.ThrowIfAny() hot path
burkenyo Jul 27, 2022
f4058ba
Correct flushing logic in ExternalLogger
burkenyo Jul 30, 2022
2cc9b6b
[sourcegen] Clean up sourcegen, especially file I/O
burkenyo Aug 8, 2022
5a87b55
[sourcegen] Update SourceGenerator API
burkenyo Aug 9, 2022
3b4b763
[sourcegen] Use typed config for C# source generator
burkenyo Aug 11, 2022
7e85cb8
[sourcegen] Update formatting for consistency with Cantera Python style
burkenyo Aug 12, 2022
89b7373
[.NET] Use separate function in CLib for external logging
burkenyo Aug 12, 2022
cb7807e
[.NET] Fix typos and add doc-comments
burkenyo Aug 12, 2022
0d3bace
[sourcegen] Tweak generate_source for better error handling
burkenyo Aug 12, 2022
cb230c0
[sourcegen] Update readme
burkenyo Aug 12, 2022
e784d02
[sourcegen] Add clarifying comments for list copy in _convert_func
burkenyo Aug 12, 2022
90a4d5b
[sourcegen] Move header file parsing to new HeaderFileParser class
burkenyo Aug 13, 2022
bd8d94a
[.NET] Remove all CR and BOM characters created by 'dotnet new'
burkenyo Aug 15, 2022
063592c
[.NET] Correct formula in example
burkenyo Aug 16, 2022
f45818a
[sourcegen] Remove superfluous local initialization in normalize_indent
burkenyo Aug 17, 2022
0b26ea0
[.NET] Update examples per review comments
burkenyo Aug 17, 2022
595c456
[.NET] Update Cantera.csproj to simplify setting up dev environment
burkenyo Aug 17, 2022
fa3b5a9
[.NET] Update readme with instructions for running tests and examples
burkenyo Aug 17, 2022
1d914da
[.NET] Add comments and update dependencies in Cantera.Tests.csproj
burkenyo Aug 17, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -5,6 +5,7 @@ doc/ctdeploy_key
*.so
*.os
*.pyc
__pycache__
*.obj
*.exe.manifest
build/
Expand Down
1 change: 1 addition & 0 deletions AUTHORS
Expand Up @@ -21,6 +21,7 @@ Vishesh Devgan (@vdevgan)
Thomas Fiala (@thomasfiala), Technische Universität München
David Fronczek
Mark E. Fuller (@mefuller), Technion
Sammo Gabay (@Burkenyo)
Matteo Giani (@MarcDuQuesne)
Dave Goodwin, California Institute of Technology
China Hagström (@chinahg), Massachusetts Institute of Technology
Expand Down
35 changes: 35 additions & 0 deletions CONTRIBUTING.md
Expand Up @@ -101,3 +101,38 @@
* Code in `.py` and `.pyx` files needs to be written to work with Python 3
* The minimum Python version that Cantera supports is Python 3.6, so code should only use features added in Python 3.6 or earlier
* Please use double quotes in all new Python code

## C#

* C# coding conventions should follow https://docs.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions
* All identifiers should follow the naming conventions in the above document including
* Prefixing with `_` for private instance fields (`_foo`, unlike C++)
* Prefixing with `s_` for private static fields (`s_bar`), `t_` for private
`[ThreadStatic]` fields (`t_baz`).
* Initial caps names for class methods (`DoSomething()`, unlike C++)
* Give the opening brace of a statement block its own line (unlike C++), except empty
blocks, which may be written as an `{ }` (for example, a constructor which calls
a base-class constructor only).
* Use only one statement per line.
* Always use statement blocks (`{ ... }`) for the bodies of statements that can take
either a statement block or a single statement (`if`, `for`, etc.)
* Use file-scoped namespaces in each new file.
* Do not take any extra Nuget dependencies in the `Cantera.csproj` project.
* Use C# XML Doc-Comments on types and members, including at least the `<summary>` tag.
burkenyo marked this conversation as resolved.
Show resolved Hide resolved
Always include a doc comment for types, but for members with self-explanatory names,
you may omit the doc comment and suppress the build error that would be thrown with
`#pragma warning disable/restore CS1591`.
* C# doc-comments use `///`, unlike Cantera's preferred use of `//!` for C++
* Do not expose any code requiring the `unsafe` keyword via a public API
(pointers, the `fixed` statement, etc). Pointers are used for the high-performance
interop layer with the native Cantera library, but such access should have a
“safe” wrapper, such as a `Span<T>` or a managed array.
* Do not allow exceptions to pass uncaught out of a callback invoked from native code,
as the interop layer cannot marshall exceptions between managed and native code,
and the process will crash. Use `CallbackException.Register()` within a catch-all
block to log the exception for later throwing back in managed code.
* The primary API for accessing Cantera is the `Application` class, which handles
required static initialization of the library. When exposing a new wrapper for CLib
functionality, do not expose a public constructor. Rather, mark the constructor
`internal` and wrap it in an appropriate factory method in the `Application` class
(`public static CreateFoo(string filename) { ... }`).
63 changes: 63 additions & 0 deletions include/cantera/base/ExternalLogger.h
@@ -0,0 +1,63 @@
// 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.

#ifndef CT_EXTERNAL_LOGGER_H
#define CT_EXTERNAL_LOGGER_H

#include "logger.h"

namespace Cantera {

//! Logger that delegates to an external source via a callback to produce log output.
//! @ingroup textlogs
class ExternalLogger : public Logger
burkenyo marked this conversation as resolved.
Show resolved Hide resolved
{
public:
explicit ExternalLogger(LogCallback writer) {
if (writer == nullptr) {
throw CanteraError("ExternalLogger::ExternalLogger",
"Argument “writer” must not be null!");
}

m_writer = writer;
}

void write(const std::string& msg) override {
m_writeBuffer.append(msg);

if (!m_writeBuffer.empty() && m_writeBuffer.back() == '\n') {
// This is a bit strange, but the terminal new line is interpreted to mean
// “end of message”, so we want to pop it from the message itself.
// The other side of the logger will be in charge of deciding whether
// “messages” will have a terminal new line or not.
m_writeBuffer.pop_back();

m_writer(LogLevel::INFO, "Info", m_writeBuffer.c_str());

m_writeBuffer.erase();
}
}

void writeendl() override {
m_writer(LogLevel::INFO, "Info", m_writeBuffer.c_str());

m_writeBuffer.erase();
}

void warn(const std::string& warning, const std::string& msg) override {
m_writer(LogLevel::WARN, warning.c_str(), msg.c_str());
}

void error(const std::string& msg) override {
m_writer(LogLevel::ERROR, "Error", msg.c_str());
}

private:
std::string m_writeBuffer;

LogCallback m_writer = nullptr;
};

}

#endif
7 changes: 7 additions & 0 deletions include/cantera/clib/clib_defs.h
Expand Up @@ -36,4 +36,11 @@
# define DERR -999.999
#endif

// Used by external logger
enum LogLevel { INFO, WARN , ERROR };

//! Represents a callback that is invoked to produce log output.
typedef void
(*LogCallback)(enum LogLevel logLevel, const char* category, const char* message);

#endif
2 changes: 2 additions & 0 deletions include/cantera/clib/ct.h
Expand Up @@ -77,6 +77,7 @@ extern "C" {
CANTERA_CAPI int thermo_getEntropies_R(int n, size_t lenm, double* s_r);
CANTERA_CAPI int thermo_getCp_R(int n, size_t lenm, double* cp_r);
CANTERA_CAPI int thermo_setElectricPotential(int n, double v);
CANTERA_CAPI int thermo_set_TP(int n, double* vals);
CANTERA_CAPI int thermo_set_RP(int n, double* vals);
CANTERA_CAPI int thermo_set_HP(int n, double* vals);
CANTERA_CAPI int thermo_set_UV(int n, double* vals);
Expand Down Expand Up @@ -155,6 +156,7 @@ extern "C" {

CANTERA_CAPI int ct_getCanteraError(int buflen, char* buf);
CANTERA_CAPI int ct_setLogWriter(void* logger);
CANTERA_CAPI int ct_setLogCallback(LogCallback writer);
CANTERA_CAPI int ct_addCanteraDirectory(size_t buflen, const char* buf);
CANTERA_CAPI int ct_getDataDirectories(int buflen, char* buf, const char* sep);
CANTERA_CAPI int ct_getCanteraVersion(int buflen, char* buf);
Expand Down
37 changes: 37 additions & 0 deletions interfaces/dotnet/.gitignore
@@ -0,0 +1,37 @@
*.swp
*.*~
project.lock.json
.DS_Store
*.pyc
nupkg/

# Visual Studio Code
.vscode

# Rider
.idea

# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates

# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
build/
bld/
[Bb]in/
[Oo]bj/
[Oo]ut/
msbuild.log
msbuild.err
msbuild.wrn

# Visual Studio 2015
.vs/
32 changes: 32 additions & 0 deletions interfaces/dotnet/Cantera.Tests/Cantera.Tests.csproj
@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<!-- We need the following three dependencies to support unit testing -->
<!-- This dependency provides the test runner -->
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.0" />
<!-- This dependency provideds the xunit annotations and assertions -->
<PackageReference Include="xunit" Version="2.4.2" />
<!-- This depenedency connects xunit to the test runner for both dotnet CLI and VisualStudio -->
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>

<!-- This dependency provides code coverage analysis -->
<PackageReference Include="coverlet.collector" Version="3.1.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>

<ProjectReference Include="../Cantera/Cantera.csproj" />
</ItemGroup>

</Project>
166 changes: 166 additions & 0 deletions interfaces/dotnet/Cantera.Tests/src/ApplicationTest.cs
@@ -0,0 +1,166 @@
// 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.Reflection;
using System.Text.RegularExpressions;
using Cantera.Interop;
using Xunit;

namespace Cantera.Tests;

[Collection("Application")]
public class ApplicationTest
{
class FooException : Exception { }

readonly static LogMessageEventArgs s_mockLog =
new(LogLevel.Warning, "Testing", "This is a test message.");

[Fact]
public void CanteraInfo_VersionRetrieved()
{
// version string should start with a number
Assert.Matches("^[1-9]", Application.Version);
}

[Fact]
public void CanteraInfo_GitCommitRetrieved()
{
Assert.NotEmpty(Application.GitCommit);
}

[Fact]
public void CanteraInfo_DataDirectoryAdded()
{
var dirs = Application.DataDirectories;
var originalCount = dirs.Count;
var longestDir = dirs.MaxBy(d => d.FullName.Length);

Assert.NotNull(longestDir);

dirs.Add(Path.Join(longestDir!.FullName, "garbazh"));

Assert.Equal(originalCount + 1, dirs.Count);
}

[Fact]
public void LogWriter_MessageLogged()
{
LogLevel? logLevel = null;
string? report = null;

void LogMessage(object? sender, LogMessageEventArgs e)
{
logLevel = e.LogLevel;
report = e.Message;
}

try
{
Application.MessageLogged += LogMessage;

ProduceRealLogOutput();

Assert.NotNull(logLevel);
Assert.NotNull(report);

Assert.Equal(LogLevel.Info, logLevel);
Assert.NotEmpty(report);
}
finally
{
Application.MessageLogged -= LogMessage;
}
}

[Fact]
public void LogWriter_ConsoleLogged()
{
var stdOut = Console.Out;
var consoleOut = new StringWriter();

try
{
Console.SetOut(consoleOut);
Application.AddConsoleLogging();
ProduceMockLogOutput();

var output = consoleOut.ToString();

var logLevel = s_mockLog.LogLevel.ToString().ToUpperInvariant();

var prefix = $"{logLevel} ({s_mockLog.Category}) ";
const string iso8601FormatString = "yyyy-MM-ddThh:mm:ss.fffzzz";
const int lengthOfIso8601FormattedString = 29;

Assert.Matches('^' + Regex.Escape(prefix), output);

var nowString = output.Substring(
prefix.Length, lengthOfIso8601FormattedString);

Assert.True(DateTimeOffset.TryParseExact(
nowString, iso8601FormatString, null, default, out _));
}
finally
{
Application.RemoveConsoleLogging();
Console.SetOut(stdOut);
}
}

[Fact]
public void LogWriter_ExceptionRegistered()
{
static void LogMessage(object? sender, LogMessageEventArgs e) =>
throw new FooException();

try
{
Application.MessageLogged += LogMessage;

var thrown =
Assert.Throws<CallbackException>(() => ProduceMockLogOutput());

Assert.NotNull(thrown.InnerException);
Assert.IsType<FooException>(thrown.InnerException);
}
finally
{
Application.MessageLogged -= LogMessage;
}
}

/// <summary>
/// Produces log output by calling into the native Cantera library to invoke
/// the logging callback.
/// </summary>
static void ProduceRealLogOutput()
{
using var thermo = Application.CreateThermoPhase("gri30.yaml");

var handle = (ThermoPhaseHandle) typeof(ThermoPhase)
.GetField("_handle", BindingFlags.NonPublic | BindingFlags.Instance)!
.GetValue(thermo)!;

InteropUtil.CheckReturn(LibCantera.thermo_print(handle, InteropConsts.True, 0));
}

/// <summary>
/// Produces log output without calling into the native Cantera library.
/// </summary>
static void ProduceMockLogOutput()
{
var eventField = typeof(Application).GetField("s_invokeMessageLoggedDelegate",
BindingFlags.Static | BindingFlags.NonPublic);

Assert.NotNull(eventField);

var del = (LibCantera.LogCallback) eventField!.GetValue(null)!;

Assert.NotNull(del);

del(s_mockLog.LogLevel, s_mockLog.Category, s_mockLog.Message);

CallbackException.ThrowIfAny();
}
}