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)} = "