From f030e2ae0f4fbd91cb4c9288c338ca1556fef6ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20Zim=C3=A1nyi?= Date: Thu, 3 Sep 2026 18:47:37 +0200 Subject: [PATCH] Read in step the arrays a call counts once MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The catalog states the one length that counts several arrays, so a method taking them takes the length from the first and states that the rest agree. `Jsonb.MakeTwoArg(Text[] keys, Text[] values)` pairs its two element by element off one count, and a shorter array is read past the end of — MEOS holds only the count it is given, so nothing on that side can see it happen. The layer emits 1355 methods over 115 classes with 6 deferred, and `Geometry` answers the area of a geometry. Its own test builds `{"a": "one", "b": "two"}` from a pair of two-element arrays and takes an `ArgumentException` for a pair of three and two. --- MEOS.NET.Tests/ParallelArrayTests.cs | 40 +++++++++++++++++++++++++ MEOS.NET/Functions/Meos.Native.g.cs | 3 ++ MEOS.NET/Functions/Meos.meos_geo.g.cs | 3 ++ MEOS.NET/Types/Geometry.g.cs | 3 ++ MEOS.NET/Types/Jsonb.g.cs | 33 ++++++++++++++++++++ tools/objectgen.py | 43 ++++++++++++++++++++++----- 6 files changed, 117 insertions(+), 8 deletions(-) create mode 100644 MEOS.NET.Tests/ParallelArrayTests.cs diff --git a/MEOS.NET.Tests/ParallelArrayTests.cs b/MEOS.NET.Tests/ParallelArrayTests.cs new file mode 100644 index 0000000..8d8853a --- /dev/null +++ b/MEOS.NET.Tests/ParallelArrayTests.cs @@ -0,0 +1,40 @@ +using MEOS.NET.Types; + +namespace MEOS.NET.Tests +{ + /// + /// Arrays a MEOS function reads in parallel are counted once, so the method + /// takes both and passes the one length — and states that they agree, since + /// a shorter one is read past the end of and MEOS cannot see it happen. + /// + [TestClass] + public class ParallelArrayTests : MeosTest + { + private static Text[] Texts(params string[] parts) + { + Text[] texts = new Text[parts.Length]; + for (int i = 0; i < parts.Length; i++) + { + texts[i] = Text.In(parts[i])!; + } + + return texts; + } + + [TestMethod] + public void AJsonbIsBuiltFromItsKeysAndItsValues() + { + Jsonb? made = Jsonb.MakeTwoArg(Texts("a", "b"), Texts("one", "two")); + + Assert.IsNotNull(made); + Assert.AreEqual("{\"a\": \"one\", \"b\": \"two\"}", made!.ToString()); + } + + [TestMethod] + public void ArraysOfDifferentLengthsAreRefusedBeforeTheCall() + { + Assert.ThrowsException( + () => Jsonb.MakeTwoArg(Texts("a", "b", "c"), Texts("one", "two"))); + } + } +} diff --git a/MEOS.NET/Functions/Meos.Native.g.cs b/MEOS.NET/Functions/Meos.Native.g.cs index e8eba5b..79b4112 100644 --- a/MEOS.NET/Functions/Meos.Native.g.cs +++ b/MEOS.NET/Functions/Meos.Native.g.cs @@ -5109,6 +5109,9 @@ private static partial class Native [return: MarshalAs(UnmanagedType.U1)] internal static partial bool GeomAzimuth(IntPtr gs1, IntPtr gs2, IntPtr result); + [LibraryImport(DllPath, EntryPoint = "geom_area", StringMarshalling = StringMarshalling.Utf8)] + internal static partial double GeomArea(IntPtr gs); + [LibraryImport(DllPath, EntryPoint = "geom_length", StringMarshalling = StringMarshalling.Utf8)] internal static partial double GeomLength(IntPtr gs); diff --git a/MEOS.NET/Functions/Meos.meos_geo.g.cs b/MEOS.NET/Functions/Meos.meos_geo.g.cs index d56ab8a..1572bda 100644 --- a/MEOS.NET/Functions/Meos.meos_geo.g.cs +++ b/MEOS.NET/Functions/Meos.meos_geo.g.cs @@ -111,6 +111,9 @@ public static double GeogPerimeter(IntPtr gs, bool use_spheroid) public static bool GeomAzimuth(IntPtr gs1, IntPtr gs2, IntPtr result) => SafeExecution(() => Native.GeomAzimuth(gs1, gs2, result)); + public static double GeomArea(IntPtr gs) + => SafeExecution(() => Native.GeomArea(gs)); + public static double GeomLength(IntPtr gs) => SafeExecution(() => Native.GeomLength(gs)); diff --git a/MEOS.NET/Types/Geometry.g.cs b/MEOS.NET/Types/Geometry.g.cs index 66ec8f2..2febd88 100644 --- a/MEOS.NET/Types/Geometry.g.cs +++ b/MEOS.NET/Types/Geometry.g.cs @@ -14,6 +14,9 @@ public class Geometry : Geo { internal Geometry(IntPtr ptr) : base(ptr) { } + public double Area() + => Meos.GeomArea(this.Ptr); + public double? Azimuth(Geo gs2) { IntPtr _result = Marshal.AllocHGlobal(8); diff --git a/MEOS.NET/Types/Jsonb.g.cs b/MEOS.NET/Types/Jsonb.g.cs index 210a745..24bc6c7 100644 --- a/MEOS.NET/Types/Jsonb.g.cs +++ b/MEOS.NET/Types/Jsonb.g.cs @@ -294,5 +294,38 @@ public long ToInt64() } } + public static Jsonb? MakeTwoArg(Text[] keys, Text[] values) + { + if (keys.Length != values.Length) + { + throw new ArgumentException( + "keys and values are read in step, so they hold the same number of elements."); + } + + IntPtr[] _keysValues = new IntPtr[keys.Length]; + for (int i = 0; i < keys.Length; i++) + { + _keysValues[i] = keys[i].Ptr; + } + + GCHandle _keys = GCHandle.Alloc(_keysValues, GCHandleType.Pinned); + IntPtr[] _valuesValues = new IntPtr[values.Length]; + for (int i = 0; i < values.Length; i++) + { + _valuesValues[i] = values[i].Ptr; + } + + GCHandle _values = GCHandle.Alloc(_valuesValues, GCHandleType.Pinned); + try + { + return MEOSFactory.WrapJsonb(Meos.JsonbMakeTwoArg(_keys.AddrOfPinnedObject(), _values.AddrOfPinnedObject(), keys.Length)); + } + finally + { + _keys.Free(); + _values.Free(); + } + } + } } diff --git a/tools/objectgen.py b/tools/objectgen.py index 7bcd57d..dbf5dbb 100644 --- a/tools/objectgen.py +++ b/tools/objectgen.py @@ -278,7 +278,8 @@ def __init__(self, name: str, ret: str, params: list[tuple[str, str]], out_params: list[tuple[str, int, str]] | None = None, structs: list[tuple[str, str]] | None = None, scalar_arrays: list[str] | None = None, - length_out: str | None = None, byte_buffer: bool = False): + length_out: str | None = None, byte_buffer: bool = False, + parallel: list[list[str]] | None = None): self.name = name self.ret = ret self.params = params @@ -297,11 +298,13 @@ def __init__(self, name: str, ret: str, params: list[tuple[str, str]], # whether that buffer is the answer itself. self.length_out = length_out self.byte_buffer = byte_buffer + # Arrays the catalog counts once, so the method reads them in step. + self.parallel = parallel or [] def needs_a_body(self) -> bool: """Whether the call needs anything allocated, pinned or read around it.""" return bool(self.structs or self.arrays or self.scalar_arrays - or self.out_params or self.length_out + or self.out_params or self.length_out or self.parallel or (self.ret.startswith("(") and self.ret.endswith(")"))) @@ -554,10 +557,16 @@ def method_for(self, cls: str, entry: dict) -> Method | None: return None ret_type, ret_expr = ret - # The length is the array's own, so it leaves the C# signature. - count_of = {codegen.csharp_param_name(a["lengthFrom"]["name"]): - codegen.csharp_param_name(a["param"]) - for a in input_arrays} + # The length is the array's own, so it leaves the C# signature. Arrays + # the catalog gives ONE length are read in parallel and counted once, so + # the method takes the length from the first of them and states that the + # rest agree — nothing in MEOS can see a shorter one, and it reads past + # the end of it. + count_of: dict[str, list[str]] = {} + for a in input_arrays: + count_of.setdefault( + codegen.csharp_param_name(a["lengthFrom"]["name"]), []).append( + codegen.csharp_param_name(a["param"])) if answers_existence: # The `bool` is not an answer: what MEOS wrote is. @@ -584,7 +593,7 @@ def method_for(self, cls: str, entry: dict) -> Method | None: continue if pname in count_of: cast = "" if cs_type == "int" else f"({cs_type}) " - args.append(f"{cast}{count_of[pname]}.Length") + args.append(f"{cast}{count_of[pname][0]}.Length") continue if pname in counted: pointee = clean(counted[pname]) @@ -629,7 +638,8 @@ def method_for(self, cls: str, entry: dict) -> Method | None: body = f"{call}|>({read})" return Method(pascal(oo), ret_type, sig, body, static, arrays, outs, structs, scalar_arrays, length_out, - ret_type == "byte[]?") + ret_type == "byte[]?", + [names for names in count_of.values() if len(names) > 1]) def inherited_names(self, cls: str) -> set[tuple]: names: set[tuple] = set() @@ -696,6 +706,23 @@ def call_body(self, method: Method) -> list[str]: read, and everything allocated is freed however the method leaves. """ setup, before, teardown = [], [], [] + for names in method.parallel: + # MEOS reads these arrays in step off one count, so a shorter one is + # read past the end of. Nothing on the MEOS side can see it. + first, rest = names[0], names[1:] + test = " || ".join( + f"{ident(first)}.Length != {ident(other)}.Length" for other in rest) + named = ident(names[0]) if len(names) == 1 else ( + ", ".join(ident(n) for n in names[:-1]) + " and " + ident(names[-1])) + setup += [ + f" if ({test})", + " {", + " throw new ArgumentException(", + f' "{named} are read in step, so they hold ' + 'the same number of elements.");', + " }", + "", + ] for name, struct in method.structs: setup.append( f" IntPtr {scratch(name)} = "