From 5a785ae6c9e1f4365f785a2413b6850e23aae748 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20Zim=C3=A1nyi?= Date: Wed, 2 Sep 2026 21:52:18 +0200 Subject: [PATCH] Read a borrowed const char * return without freeing it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Utf8 string marshaller frees the pointer a `char *` return hands back. That is right for a string MEOS mallocs for the caller and fatal for the `const char *` it returns out of a static table: `temporal_interp` answers with an element of `MEOS_INTERPTYPE_NAMES`, and freeing that ends the process — `Test host process crashed : free(): invalid pointer`. The eleven functions whose return is `const char *` — `temporal_interp`, `temporal_subtype`, `temporal_basetype_name`, `meostype_name` and their kin — come back as the pointer itself and are read with `Marshal.PtrToStringUTF8`, which copies and frees nothing. Three tests read an interpolation name: off a linear sequence, off a discrete one, and off two temporal values in turn. They exercise the native library, so the test step takes the LD_LIBRARY_PATH of the prefix the provisioning built, as the smoke step does, and every test class opens a MEOS session of its own: MEOS keeps the timezone its text I/O goes through and the error handler per thread, and the test host runs a test on whichever thread it has free, so a session opened once for the assembly reaches the thread that opened it and no other. --- .github/workflows/ci.yml | 2 + MEOS.NET.Tests/BorrowedStringTests.cs | 39 ++++++++++++++++++++ MEOS.NET.Tests/MeosTest.cs | 21 +++++++++++ MEOS.NET.Tests/TemporalGeometryPointTests.cs | 21 ----------- MEOS.NET/Types/Temporal/Temporal.cs | 10 ++++- tools/codegen.py | 19 ++++++++++ 6 files changed, 90 insertions(+), 22 deletions(-) create mode 100644 MEOS.NET.Tests/BorrowedStringTests.cs create mode 100644 MEOS.NET.Tests/MeosTest.cs delete mode 100644 MEOS.NET.Tests/TemporalGeometryPointTests.cs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 272d669..97021ad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,6 +38,8 @@ jobs: - name: Run tests run: dotnet test MEOS.NET.sln -c Release --no-build + env: + LD_LIBRARY_PATH: ${{ steps.provision.outputs.libmeos-prefix }}/lib - name: Smoke-test the FFI run: dotnet run --project ExampleApp/ExampleApp.csproj -c Release --no-build diff --git a/MEOS.NET.Tests/BorrowedStringTests.cs b/MEOS.NET.Tests/BorrowedStringTests.cs new file mode 100644 index 0000000..edc77ee --- /dev/null +++ b/MEOS.NET.Tests/BorrowedStringTests.cs @@ -0,0 +1,39 @@ +using MEOS.NET.Enums; +using MEOS.NET.Types.Temporal.Number.Float; + +namespace MEOS.NET.Tests +{ + /// + /// The MEOS functions that answer with a `const char *` hand back a pointer + /// into a static table, so the caller reads it and does not free it. Freeing + /// it takes the process down, which is what these read. + /// + [TestClass] + public class BorrowedStringTests : MeosTest + { + [TestMethod] + public void InterpolationOfALinearSequenceIsRead() + { + TemporalFloat temp = TemporalFloat.FromString("[25.0@2024-12-06, 27.0@2024-12-07]"); + + Assert.AreEqual(InterpolationType.Linear, temp.Interpolation()); + } + + [TestMethod] + public void InterpolationOfADiscreteSequenceIsRead() + { + TemporalFloat temp = TemporalFloat.FromString("{25.0@2024-12-06, 27.0@2024-12-07}"); + + Assert.AreEqual(InterpolationType.Discrete, temp.Interpolation()); + } + + [TestMethod] + public void TheSameBorrowedNameIsReadableTwice() + { + TemporalFloat first = TemporalFloat.FromString("[25.0@2024-12-06, 27.0@2024-12-07]"); + TemporalFloat second = TemporalFloat.FromString("[1.0@2024-12-06, 2.0@2024-12-07]"); + + Assert.AreEqual(first.Interpolation(), second.Interpolation()); + } + } +} diff --git a/MEOS.NET.Tests/MeosTest.cs b/MEOS.NET.Tests/MeosTest.cs new file mode 100644 index 0000000..5b25245 --- /dev/null +++ b/MEOS.NET.Tests/MeosTest.cs @@ -0,0 +1,21 @@ +using MEOS.NET.Lifecycle; + +namespace MEOS.NET.Tests +{ + /// + /// The base every test class derives from, so each test runs against an open + /// MEOS session. + /// + /// MEOS keeps its session state — the timezone its text I/O reads and writes + /// through, and the error handler — per thread, and the test host runs a test + /// on whichever thread it has free. A session opened once for the assembly + /// therefore reaches the thread that opened it and no other: elsewhere a + /// timestamp is read in the machine's own timezone rather than the suite's, + /// and an error raised there reaches no handler at all. + /// + public abstract class MeosTest + { + [TestInitialize] + public void OpenMeosSession() => MEOSLifecycle.Initialize("UTC"); + } +} diff --git a/MEOS.NET.Tests/TemporalGeometryPointTests.cs b/MEOS.NET.Tests/TemporalGeometryPointTests.cs deleted file mode 100644 index a9bedc2..0000000 --- a/MEOS.NET.Tests/TemporalGeometryPointTests.cs +++ /dev/null @@ -1,21 +0,0 @@ -using MEOS.NET.Lifecycle; -using MEOS.NET.Types.General; - -namespace MEOS.NET.Tests -{ - [TestClass] - public class TemporalGeometryPointTests - { - [ClassInitialize] - public static void InitializeTests(TestContext context) - { - MEOSLifecycle.Initialize("UTC"); - } - - [ClassCleanup] - public static void ClassCleanup() - { - MEOSLifecycle.Terminate(); - } - } -} \ No newline at end of file diff --git a/MEOS.NET/Types/Temporal/Temporal.cs b/MEOS.NET/Types/Temporal/Temporal.cs index 99ca69c..3925903 100644 --- a/MEOS.NET/Types/Temporal/Temporal.cs +++ b/MEOS.NET/Types/Temporal/Temporal.cs @@ -1,4 +1,6 @@ using MEOS.NET.Enums; +using MEOS.NET.Errors; +using MEOS.NET.Exceptions; using MEOS.NET.Helpers; using MEOS.NET.Internal; using MEOS.NET.Types.General; @@ -123,7 +125,13 @@ public TimestampTzSpan BoundingBox() public InterpolationType Interpolation() { - var interpolationStr = MEOSExposedFunctions.temporal_interp(this._ptr); + // temporal_interp names one of the interpolations MEOS defines, out of + // its own static table, so the name is there for every temporal value. + var interpolationStr = MEOSExposedFunctions.temporal_interp(this._ptr) + ?? throw new MEOSUnspecifiedInternalException( + (int)MEOSErrorCodes.UnspecifiedInternalError, + MEOSErrorCodes.UnspecifiedInternalError, + "MEOS named no interpolation for this temporal value"); return EnumConverter.InterpolationTypeFromString(interpolationStr); } diff --git a/tools/codegen.py b/tools/codegen.py index a6755c1..a4cadf2 100755 --- a/tools/codegen.py +++ b/tools/codegen.py @@ -133,6 +133,17 @@ def is_string_pointer(c_type: str) -> bool: return t in ("char *", "char**", "char *const *", "const char *") +def is_borrowed_string(c_type: str) -> bool: + """A ``const char *`` return the caller does not own. + + The Utf8 string marshaller frees what it is handed back, which is right for + the ``char *`` MEOS mallocs for the caller and fatal for the ``const char *`` + it returns out of a static table — ``interptype_name`` hands back an element + of ``MEOS_INTERPTYPE_NAMES``. A borrowed return comes back as the pointer + itself and is read without a free.""" + return " ".join(c_type.split()).startswith("const char *") + + def csharp_type_for(canonical: str) -> str: """Translate a libclang-canonical C type to a C# type for LibraryImport signatures.""" t = canonical.strip() @@ -155,6 +166,8 @@ def csharp_param_type(c_type: str, canonical: str) -> str: def csharp_return_type(c_type: str, canonical: str) -> str: + if is_borrowed_string(c_type): + return "IntPtr" if is_string_pointer(c_type): return "string" return csharp_type_for(canonical) @@ -472,6 +485,12 @@ def _copy(indent: str) -> list[str]: def _emit_simple_passthrough(f: dict, ext_params: str, ext_args: str, default_rt: str | None = None) -> list[str]: name = f["name"] rt = default_rt or csharp_return_type(f["returnType"]["c"], f["returnType"]["canonical"]) + if default_rt is None and is_borrowed_string(f["returnType"]["c"]): + return [ + f" public static string? {name}({ext_params})", + f" => Marshal.PtrToStringUTF8(" + f"SafeExecution(() => MEOSExternalFunctions.{name}({ext_args})));", + ] if rt == "void": return [ f" public static void {name}({ext_params})",