From ac2c5709c17f267777fb93d031887cdcb06aa255 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20Zim=C3=A1nyi?= Date: Thu, 3 Sep 2026 13:33:43 +0200 Subject: [PATCH 1/2] Answer the nearest-neighbour walk as the cursor it is The catalog gives the R-tree and SP-tree kNN cursors classes, so the object layer carries `RTreeNNCursor` and `SPNNCursor`: a cursor is opened from a tree, closed when the walk ends, and the three entries sit on the cursor rather than among the tree's twenty methods. 115 classes and 1342 methods against 113 and 1338, 18 deferred against 22. `TBigint.Values` reaches the layer with them, its count declared the way its definition writes it, so every temporal number answers its distinct values as an array of them. `next` stays deferred on both cursors: it answers the id AND the distance through two out-parameters, and one body writing both is the same shape the value-and-time splits need. --- MEOS.NET/Functions/Meos.meos.g.cs | 15 ++++++++-- MEOS.NET/Types/MEOSFactory.g.cs | 48 +++++++++++++++++++++++++++++++ MEOS.NET/Types/RTreeNNCursor.g.cs | 24 ++++++++++++++++ MEOS.NET/Types/SPNNCursor.g.cs | 24 ++++++++++++++++ MEOS.NET/Types/TBigint.g.cs | 3 ++ 5 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 MEOS.NET/Types/RTreeNNCursor.g.cs create mode 100644 MEOS.NET/Types/SPNNCursor.g.cs diff --git a/MEOS.NET/Functions/Meos.meos.g.cs b/MEOS.NET/Functions/Meos.meos.g.cs index 757a524..dd03274 100644 --- a/MEOS.NET/Functions/Meos.meos.g.cs +++ b/MEOS.NET/Functions/Meos.meos.g.cs @@ -3136,8 +3136,19 @@ public static int[] TintValues(IntPtr temp) finally { Marshal.FreeHGlobal(_cnt); } } - public static IntPtr TbigintValues(IntPtr temp, IntPtr count) - => SafeExecution(() => Native.TbigintValues(temp, count)); + public static long[] TbigintValues(IntPtr temp) + { + IntPtr _cnt = Marshal.AllocHGlobal(sizeof(int)); + try + { + IntPtr _p = SafeExecution(() => Native.TbigintValues(temp, _cnt)); + int _n = Marshal.ReadInt32(_cnt); + long[] _out = new long[_n]; + Marshal.Copy(_p, _out, 0, _n); + return _out; + } + finally { Marshal.FreeHGlobal(_cnt); } + } public static double TnumberAvgValue(IntPtr temp) => SafeExecution(() => Native.TnumberAvgValue(temp)); diff --git a/MEOS.NET/Types/MEOSFactory.g.cs b/MEOS.NET/Types/MEOSFactory.g.cs index bf0c29c..2768a23 100644 --- a/MEOS.NET/Types/MEOSFactory.g.cs +++ b/MEOS.NET/Types/MEOSFactory.g.cs @@ -329,6 +329,30 @@ public static class MEOSFactory return wrapped; } + /// The value at as the RTreeNNCursor + /// class the model gives its runtime type. + public static RTreeNNCursor? WrapRTreeNNCursor(IntPtr ptr) + { + if (ptr == IntPtr.Zero) + { + return null; + } + + return new RTreeNNCursor(ptr); + } + + /// Every element of a C array of RTreeNNCursor pointers, wrapped. + public static RTreeNNCursor?[] WrapRTreeNNCursorArray(IntPtr[] ptrs) + { + RTreeNNCursor?[] wrapped = new RTreeNNCursor?[ptrs.Length]; + for (int i = 0; i < ptrs.Length; i++) + { + wrapped[i] = WrapRTreeNNCursor(ptrs[i]); + } + + return wrapped; + } + /// The value at as the Raquet /// class the model gives its runtime type. public static Raquet? WrapRaquet(IntPtr ptr) @@ -353,6 +377,30 @@ public static class MEOSFactory return wrapped; } + /// The value at as the SPNNCursor + /// class the model gives its runtime type. + public static SPNNCursor? WrapSPNNCursor(IntPtr ptr) + { + if (ptr == IntPtr.Zero) + { + return null; + } + + return new SPNNCursor(ptr); + } + + /// Every element of a C array of SPNNCursor pointers, wrapped. + public static SPNNCursor?[] WrapSPNNCursorArray(IntPtr[] ptrs) + { + SPNNCursor?[] wrapped = new SPNNCursor?[ptrs.Length]; + for (int i = 0; i < ptrs.Length; i++) + { + wrapped[i] = WrapSPNNCursor(ptrs[i]); + } + + return wrapped; + } + /// The value at as the SPTree /// class the model gives its runtime type. public static SPTree? WrapSPTree(IntPtr ptr) diff --git a/MEOS.NET/Types/RTreeNNCursor.g.cs b/MEOS.NET/Types/RTreeNNCursor.g.cs new file mode 100644 index 0000000..0c638b4 --- /dev/null +++ b/MEOS.NET/Types/RTreeNNCursor.g.cs @@ -0,0 +1,24 @@ +#nullable enable + +using System.Runtime.InteropServices; + +using MEOS.NET.Enums; +using MEOS.NET.Functions; +using MEOS.NET.Structures; + +namespace MEOS.NET.Types +{ + /// A walk over an R-tree's entries in order of distance from a query. MEOS registers it in no enum, so it names no temptype. + [System.CodeDom.Compiler.GeneratedCode("MEOS.NET.ObjectGen", "0.1.0")] + public class RTreeNNCursor : Value + { + internal RTreeNNCursor(IntPtr ptr) : base(ptr) { } + + public void Close() + => Meos.RtreeNnCursorClose(this.Ptr); + + public static RTreeNNCursor? Open(RTree rtree, IntPtr query) + => MEOSFactory.WrapRTreeNNCursor(Meos.RtreeNnCursorOpen(rtree.Ptr, query)); + + } +} diff --git a/MEOS.NET/Types/SPNNCursor.g.cs b/MEOS.NET/Types/SPNNCursor.g.cs new file mode 100644 index 0000000..6ad4c3a --- /dev/null +++ b/MEOS.NET/Types/SPNNCursor.g.cs @@ -0,0 +1,24 @@ +#nullable enable + +using System.Runtime.InteropServices; + +using MEOS.NET.Enums; +using MEOS.NET.Functions; +using MEOS.NET.Structures; + +namespace MEOS.NET.Types +{ + /// The SP-tree's nearest-neighbour walk, the R-tree cursor's sibling. MEOS registers it in no enum, so it names no temptype. + [System.CodeDom.Compiler.GeneratedCode("MEOS.NET.ObjectGen", "0.1.0")] + public class SPNNCursor : Value + { + internal SPNNCursor(IntPtr ptr) : base(ptr) { } + + public void Close() + => Meos.SptreeNnCursorClose(this.Ptr); + + public static SPNNCursor? Open(SPTree sptree, IntPtr query) + => MEOSFactory.WrapSPNNCursor(Meos.SptreeNnCursorOpen(sptree.Ptr, query)); + + } +} diff --git a/MEOS.NET/Types/TBigint.g.cs b/MEOS.NET/Types/TBigint.g.cs index 5105d83..8fa2835 100644 --- a/MEOS.NET/Types/TBigint.g.cs +++ b/MEOS.NET/Types/TBigint.g.cs @@ -93,6 +93,9 @@ public long StartValue() } } + public long[] Values() + => Meos.TbigintValues(this.Ptr); + public static Temporal? FromBaseTemp(long i, Temporal temp) => MEOSFactory.WrapTemporal(Meos.TbigintFromBaseTemp(i, temp.Ptr)); From 913a83e4af0a21689187478b7b028a2fbb6bdbdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20Zim=C3=A1nyi?= Date: Thu, 3 Sep 2026 14:13:51 +0200 Subject: [PATCH 2/2] Answer the several results of one call as one tuple MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A split states its pieces through the return and the bin each piece falls in through an array MEOS fills, and the flat wrapper already hands both over as a tuple. The object layer had no reading for a tuple return, so the methods that answer one deferred — the time and value splits of every temporal number, the sequence-set span walks, the jsonb each. They now answer `(Temporal?[], DateTime[])` and its siblings, each item read the way a return of its own type is: an array of MEOS values wrapped element by element, an array of instants read as instants, an array of scalars handed on. The element types come from the shape the catalog states — `arrayReturn.element` for the return and the out-array parameters for the rest — so nothing re-parses a C declaration. The five bodies a method could have were mutually exclusive, and two deferrals existed only to say so: a struct argument beside a counted array, and a length out-parameter beside either. One composed body replaces them. Everything the call needs is allocated or pinned first, the call is made ONCE, what it answers is read, and everything allocated is freed however the method leaves — so `timeSplit`, which takes an `Interval` by value and answers two arrays, needs no reading its parts do not already have. A call that allocates nothing has nothing to free, and gets the answer alone as its body. The layer emits 1351 methods over 115 classes with 9 deferred, against 1342 and 18. Its own test states that the pieces and the bins of a split arrive together and in step, from the one call that makes them. --- MEOS.NET.Tests/MultipleAnswerTests.cs | 30 +++ MEOS.NET/Types/Jsonb.g.cs | 14 ++ MEOS.NET/Types/MEOSConvert.g.cs | 24 +++ MEOS.NET/Types/TFloat.g.cs | 23 +++ MEOS.NET/Types/TInt.g.cs | 23 +++ MEOS.NET/Types/TRGeometrySeqSet.g.cs | 22 ++ MEOS.NET/Types/TSequenceSet.g.cs | 22 ++ MEOS.NET/Types/Temporal.g.cs | 16 ++ tools/objectgen.py | 287 ++++++++++++++------------ 9 files changed, 333 insertions(+), 128 deletions(-) create mode 100644 MEOS.NET.Tests/MultipleAnswerTests.cs diff --git a/MEOS.NET.Tests/MultipleAnswerTests.cs b/MEOS.NET.Tests/MultipleAnswerTests.cs new file mode 100644 index 0000000..ded1b29 --- /dev/null +++ b/MEOS.NET.Tests/MultipleAnswerTests.cs @@ -0,0 +1,30 @@ +using MEOS.NET.Structures; +using MEOS.NET.Types; + +namespace MEOS.NET.Tests +{ + /// + /// A call that answers several values answers them together, so the method + /// answers a tuple and the call is made once — the halves of one answer + /// come from one call. + /// + [TestClass] + public class MultipleAnswerTests : MeosTest + { + [TestMethod] + public void SplittingByTimeAnswersThePiecesAndTheirBins() + { + Temporal trip = TFloat.In( + "[1@2024-12-06, 2@2024-12-07, 3@2024-12-08]")!; + + (Temporal?[] pieces, DateTime[] bins) = trip.TimeSplit( + Interval.In("1 day")!.Value, + new DateTime(2024, 12, 6, 0, 0, 0, DateTimeKind.Utc)); + + Assert.AreEqual(pieces.Length, bins.Length); + Assert.IsTrue(pieces.Length > 1); + Assert.AreEqual(new DateTime(2024, 12, 6, 0, 0, 0, DateTimeKind.Utc), + bins[0]); + } + } +} diff --git a/MEOS.NET/Types/Jsonb.g.cs b/MEOS.NET/Types/Jsonb.g.cs index 210a745..d08e44c 100644 --- a/MEOS.NET/Types/Jsonb.g.cs +++ b/MEOS.NET/Types/Jsonb.g.cs @@ -86,6 +86,20 @@ public bool Contained(Jsonb jb2) } } + public (Text?[], Jsonb?[]) Each() + { + var _answered = Meos.JsonbEach(this.Ptr); + + return (MEOSFactory.WrapTextArray(_answered.Item1), MEOSFactory.WrapJsonbArray(_answered.Item2)); + } + + public (Text?[], Text?[]) EachText() + { + var _answered = Meos.JsonbEachText(this.Ptr); + + return (MEOSFactory.WrapTextArray(_answered.Item1), MEOSFactory.WrapTextArray(_answered.Item2)); + } + public bool Exists(Text key) => Meos.JsonbExists(this.Ptr, key.Ptr); diff --git a/MEOS.NET/Types/MEOSConvert.g.cs b/MEOS.NET/Types/MEOSConvert.g.cs index ec372f1..b65354c 100644 --- a/MEOS.NET/Types/MEOSConvert.g.cs +++ b/MEOS.NET/Types/MEOSConvert.g.cs @@ -33,6 +33,30 @@ internal static int ToDateADT(DateOnly day) => Meos.DateIn( day.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)); + /// Each MEOS TimestampTz of an array, as a DateTime. + internal static DateTime[] ToDateTimeArray(long[] moments) + { + DateTime[] values = new DateTime[moments.Length]; + for (int i = 0; i < moments.Length; i++) + { + values[i] = ToDateTime(moments[i]); + } + + return values; + } + + /// Each MEOS DateADT of an array, as a DateOnly. + internal static DateOnly[] ToDateOnlyArray(int[] days) + { + DateOnly[] values = new DateOnly[days.Length]; + for (int i = 0; i < days.Length; i++) + { + values[i] = ToDateOnly(days[i]); + } + + return values; + } + /// The struct MEOS answers through a pointer, as a value. The /// memory behind the pointer stays MEOS's, as it does for every other /// value the layer reads back. diff --git a/MEOS.NET/Types/TFloat.g.cs b/MEOS.NET/Types/TFloat.g.cs index 4b579cc..c0c0cea 100644 --- a/MEOS.NET/Types/TFloat.g.cs +++ b/MEOS.NET/Types/TFloat.g.cs @@ -140,6 +140,13 @@ public double StartValue() } } + public (Temporal?[], double[]) ValueSplit(double size, double origin) + { + var _answered = Meos.TfloatValueSplit(this.Ptr, size, origin); + + return (MEOSFactory.WrapTemporalArray(_answered.Item1), _answered.Item2); + } + public TBox?[] ValueTimeBoxes(double vsize, Interval duration, double vorigin, DateTime torigin) { IntPtr _duration = Marshal.AllocHGlobal(Marshal.SizeOf()); @@ -154,6 +161,22 @@ public double StartValue() } } + public (Temporal?[], double[], DateTime[]) ValueTimeSplit(double vsize, Interval duration, double vorigin, DateTime torigin) + { + IntPtr _duration = Marshal.AllocHGlobal(Marshal.SizeOf()); + try + { + Marshal.StructureToPtr(duration, _duration, false); + var _answered = Meos.TfloatValueTimeSplit(this.Ptr, vsize, _duration, vorigin, MEOSConvert.ToTimestampTz(torigin)); + + return (MEOSFactory.WrapTemporalArray(_answered.Item1), _answered.Item2, MEOSConvert.ToDateTimeArray(_answered.Item3)); + } + finally + { + Marshal.FreeHGlobal(_duration); + } + } + public double[] Values() => Meos.TfloatValues(this.Ptr); diff --git a/MEOS.NET/Types/TInt.g.cs b/MEOS.NET/Types/TInt.g.cs index b4b43a5..f9c4758 100644 --- a/MEOS.NET/Types/TInt.g.cs +++ b/MEOS.NET/Types/TInt.g.cs @@ -110,6 +110,13 @@ public int StartValue() } } + public (Temporal?[], int[]) ValueSplit(int vsize, int vorigin) + { + var _answered = Meos.TintValueSplit(this.Ptr, vsize, vorigin); + + return (MEOSFactory.WrapTemporalArray(_answered.Item1), _answered.Item2); + } + public TBox?[] ValueTimeBoxes(int vsize, Interval duration, int vorigin, DateTime torigin) { IntPtr _duration = Marshal.AllocHGlobal(Marshal.SizeOf()); @@ -124,6 +131,22 @@ public int StartValue() } } + public (Temporal?[], int[], DateTime[]) ValueTimeSplit(int size, Interval duration, int vorigin, DateTime torigin) + { + IntPtr _duration = Marshal.AllocHGlobal(Marshal.SizeOf()); + try + { + Marshal.StructureToPtr(duration, _duration, false); + var _answered = Meos.TintValueTimeSplit(this.Ptr, size, _duration, vorigin, MEOSConvert.ToTimestampTz(torigin)); + + return (MEOSFactory.WrapTemporalArray(_answered.Item1), _answered.Item2, MEOSConvert.ToDateTimeArray(_answered.Item3)); + } + finally + { + Marshal.FreeHGlobal(_duration); + } + } + public int[] Values() => Meos.TintValues(this.Ptr); diff --git a/MEOS.NET/Types/TRGeometrySeqSet.g.cs b/MEOS.NET/Types/TRGeometrySeqSet.g.cs index c67c358..e71fe04 100644 --- a/MEOS.NET/Types/TRGeometrySeqSet.g.cs +++ b/MEOS.NET/Types/TRGeometrySeqSet.g.cs @@ -33,5 +33,27 @@ internal TRGeometrySeqSet(IntPtr ptr) : base(ptr) { } } } + public Temporal? MakeGaps(Temporal[] instants, InterpType interp, Interval maxt, double maxdist) + { + IntPtr _maxt = Marshal.AllocHGlobal(Marshal.SizeOf()); + IntPtr[] _instantsValues = new IntPtr[instants.Length]; + for (int i = 0; i < instants.Length; i++) + { + _instantsValues[i] = instants[i].Ptr; + } + + GCHandle _instants = GCHandle.Alloc(_instantsValues, GCHandleType.Pinned); + try + { + Marshal.StructureToPtr(maxt, _maxt, false); + return MEOSFactory.WrapTemporal(Meos.TrgeometryseqsetMakeGaps(this.Ptr, _instants.AddrOfPinnedObject(), instants.Length, (int) interp, _maxt, maxdist)); + } + finally + { + Marshal.FreeHGlobal(_maxt); + _instants.Free(); + } + } + } } diff --git a/MEOS.NET/Types/TSequenceSet.g.cs b/MEOS.NET/Types/TSequenceSet.g.cs index 36ad5a4..c4e5ebb 100644 --- a/MEOS.NET/Types/TSequenceSet.g.cs +++ b/MEOS.NET/Types/TSequenceSet.g.cs @@ -33,5 +33,27 @@ internal TSequenceSet(IntPtr ptr) : base(ptr) { } } } + public static Temporal? MakeGaps(Temporal[] instants, InterpType interp, Interval maxt, double maxdist) + { + IntPtr _maxt = Marshal.AllocHGlobal(Marshal.SizeOf()); + IntPtr[] _instantsValues = new IntPtr[instants.Length]; + for (int i = 0; i < instants.Length; i++) + { + _instantsValues[i] = instants[i].Ptr; + } + + GCHandle _instants = GCHandle.Alloc(_instantsValues, GCHandleType.Pinned); + try + { + Marshal.StructureToPtr(maxt, _maxt, false); + return MEOSFactory.WrapTemporal(Meos.TsequencesetMakeGaps(_instants.AddrOfPinnedObject(), instants.Length, (int) interp, _maxt, maxdist)); + } + finally + { + Marshal.FreeHGlobal(_maxt); + _instants.Free(); + } + } + } } diff --git a/MEOS.NET/Types/Temporal.g.cs b/MEOS.NET/Types/Temporal.g.cs index cb39b4a..095de24 100644 --- a/MEOS.NET/Types/Temporal.g.cs +++ b/MEOS.NET/Types/Temporal.g.cs @@ -391,6 +391,22 @@ public DateTime StartTimestamptz() } } + public (Temporal?[], DateTime[]) TimeSplit(Interval duration, DateTime torigin) + { + IntPtr _duration = Marshal.AllocHGlobal(Marshal.SizeOf()); + try + { + Marshal.StructureToPtr(duration, _duration, false); + var _answered = Meos.TemporalTimeSplit(this.Ptr, _duration, MEOSConvert.ToTimestampTz(torigin)); + + return (MEOSFactory.WrapTemporalArray(_answered.Item1), MEOSConvert.ToDateTimeArray(_answered.Item2)); + } + finally + { + Marshal.FreeHGlobal(_duration); + } + } + public long[] Timestamps() => Meos.TemporalTimestamps(this.Ptr); diff --git a/tools/objectgen.py b/tools/objectgen.py index 2d5ed65..5763f01 100644 --- a/tools/objectgen.py +++ b/tools/objectgen.py @@ -297,6 +297,12 @@ def __init__(self, name: str, ret: str, params: list[tuple[str, str]], self.length_out = length_out self.byte_buffer = byte_buffer + 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_param or self.length_out + or (self.ret.startswith("(") and self.ret.endswith(")"))) + class Generator: def __init__(self, model: Model): @@ -328,6 +334,8 @@ def map_return(self, f: dict, wrapper_ret: str) -> tuple[str, str] | None: cls = self.m.class_for_ctype(c) if cls and wrapper_ret == "IntPtr": return (f"{cls}?", f"MEOSFactory.Wrap{cls}($)") + if wrapper_ret.startswith("(") and wrapper_ret.endswith(")"): + return self.map_tuple_return(f, wrapper_ret) if wrapper_ret == "IntPtr[]": # An array of MEOS values reaches the wrapper as an array of pointers, # whether MEOS returns pointers (`T **`) or the values themselves @@ -362,6 +370,52 @@ def wrapped_out_reader(self, pointee: str) -> tuple[str, int, str] | None: return None return (cls, 8, f"MEOSFactory.Wrap{cls}(Marshal.ReadIntPtr({{0}}))") + def map_tuple_return(self, f: dict, wrapper_ret: str) -> tuple[str, str] | None: + """The C# type and per-item readers for a call answering SEVERAL arrays. + + MEOS answers the primary array through the return and each parallel one + through a pointer it fills, and the wrapper already hands them over as + one tuple. Each item is then read the way a return of its own type is: + an array of MEOS values wraps element by element, an array of instants + reads as instants, and an array of plain scalars is handed on.""" + shape = f.get("shape") or {} + element = ((shape.get("arrayReturn") or {}).get("element") or {}).get("c") + params = {p["name"]: p["cType"] for p in f.get("params", [])} + # `TYPE **bins` is an out-ARRAY of `TYPE`, so two stars come off: one + # for the handle MEOS writes through, one for the array itself. + c_types = [element] + [ + clean(params.get(a["param"], "")).rstrip("* ").strip() + for a in shape.get("outputArrays") or []] + pieces = [p.strip() for p in wrapper_ret[1:-1].split(",")] + if len(pieces) != len(c_types) or not all(c_types): + return None + types, readers = [], [] + for i, (piece, c_type) in enumerate(zip(pieces, c_types)): + mapped = self.map_array_item(piece, clean(c_type), f"$.Item{i + 1}") + if mapped is None: + return None + types.append(mapped[0]) + readers.append(mapped[1]) + # The body evaluates the call once and reads each item off it, so the + # template carries the two halves apart: the call, then the reads. + reads = ", ".join(r.replace("$", "_answered") for r in readers) + return (f"({', '.join(types)})", f"$|>({reads})") + + def map_array_item(self, piece: str, c_type: str, + item: str) -> tuple[str, str] | None: + """One item of an answered tuple: its C# type and how it is read.""" + if piece == "IntPtr[]": + cls = (self.m.class_for_ctype(c_type) + or self.m.class_for_ctype(f"{c_type} *")) + return (f"{cls}?[]", f"MEOSFactory.Wrap{cls}Array({item})") if cls else None + if c_type in ("TimestampTz", "Timestamp") and piece == "long[]": + return ("DateTime[]", f"MEOSConvert.ToDateTimeArray({item})") + if c_type == "DateADT" and piece == "int[]": + return ("DateOnly[]", f"MEOSConvert.ToDateOnlyArray({item})") + if piece.endswith("[]") and is_scalar(piece[:-2]): + return (piece, item) + return None + def value_struct(self, c_type: str) -> str | None: """The struct a single pointer to a scalar-only struct carries. @@ -540,16 +594,6 @@ def method_for(self, cls: str, entry: dict) -> Method | None: if result_out is not None: name, (_, size, reader) = result_out out = (name, size, reader.format(scratch(name))) - if structs and (arrays or out or scalar_arrays or length_out): - self.deferred[cls].append( - f"{oo}: a struct argument beside a counted array or an " - "out-parameter needs one body doing both") - return None - if length_out and (arrays or out): - self.deferred[cls].append( - f"{oo}: a length out-parameter beside a counted array or a " - "value out-parameter needs one body doing both") - return None return Method(pascal(oo), ret_type, sig, ret_expr.replace("$", call), static, arrays, out, structs, scalar_arrays, length_out, ret_type == "byte[]?") @@ -601,145 +645,108 @@ def emit_class(self, cls: str) -> str: new = "new " if (m.name, tuple(t for t, _ in m.params)) in inherited else "" kind = "static " if m.static else "" lines.append(f" public {new}{kind}{m.ret} {m.name}({args})") - if m.out_param: - lines.extend(self.out_param_body(m)) - elif m.length_out: - lines.extend(self.buffer_body(m)) - elif m.arrays or m.scalar_arrays: - lines.extend(self.array_body(m)) - elif m.structs: - lines.extend(self.struct_body(m)) + if m.needs_a_body(): + lines.extend(self.call_body(m)) else: lines.append(f" => {m.body};") lines.append("") lines += [" }", "}", ""] return "\n".join(lines) - def out_param_body(self, method: Method) -> list[str]: - """The body of a method whose value MEOS writes through an out-parameter. + def call_body(self, method: Method) -> list[str]: + """The body of a method whose call needs something around it. - MEOS answers `false` when the value does not exist and leaves the - out-parameter untouched, so the method answers null there and the value - otherwise.""" - name, size, reader = method.out_param - return [ - " {", - f" IntPtr {scratch(name)} = Marshal.AllocHGlobal({size});", - " try", - " {", - f" if (!{method.body})", - " {", - " return null;", - " }", - "", - f" return {reader};", - " }", - " finally", - " {", - f" Marshal.FreeHGlobal({scratch(name)});", - " }", - " }", - ] - - def struct_body(self, method: Method) -> list[str]: - """The body of a method taking a struct MEOS reads through a pointer. - - The caller hands over a value, so the call gets the address of a copy - that lives exactly as long as it does. MEOS reads what it needs before - returning — the values it keeps it copies — so nothing outlives the - frame.""" - lines = [" {"] + One call can need several of these at once — a struct argument beside a + counted array, an array beside the length MEOS states through a pointer + — so the body is composed rather than chosen: everything the call needs + is allocated or pinned first, the call is made ONCE, what it answers is + read, and everything allocated is freed however the method leaves. + """ + setup, before, teardown = [], [], [] for name, struct in method.structs: - lines.append( + setup.append( f" IntPtr {scratch(name)} = " f"Marshal.AllocHGlobal(Marshal.SizeOf<{struct}>());") - lines.append(" try") - lines.append(" {") - for name, _ in method.structs: - lines.append( - f" Marshal.StructureToPtr({ident(name)}, {scratch(name)}, false);") - lines.append(f" return {method.body};") - lines.append(" }") - lines.append(" finally") - lines.append(" {") - for name, _ in method.structs: - lines.append(f" Marshal.FreeHGlobal({scratch(name)});") - lines += [" }", " }"] - return lines - - def buffer_body(self, method: Method) -> list[str]: - """The body of a method whose call states a length through a pointer. - - MEOS writes the length of what it answers into a buffer the caller - supplies. A string carries its own length, so there the buffer is - written and dropped; a byte array does not, so there it says how much of - what MEOS answered to copy.""" - name = method.length_out - lines = [ - " {", - f" IntPtr {scratch(name)} = Marshal.AllocHGlobal(sizeof(long));", - " try", - " {", - ] - if method.byte_buffer: - lines += [ - f" IntPtr _bytes = {method.body};", - " if (_bytes == IntPtr.Zero)", - " {", - " return null;", - " }", - "", - f" byte[] _wkb = new byte[Marshal.ReadInt64({scratch(name)})];", - " Marshal.Copy(_bytes, _wkb, 0, _wkb.Length);", - " return _wkb;", - ] - else: - lines.append(f" return {method.body};") - lines += [ - " }", - " finally", - " {", - f" Marshal.FreeHGlobal({scratch(name)});", - " }", - " }", - ] - return lines - - def array_body(self, method: Method) -> list[str]: - """The body of a method taking a counted array. - - MEOS reads an array of its own values through a pointer to the first - element, so the wrappers' pointers are gathered into one array and pinned - for the call. MEOS copies what it keeps, so the pin lasts exactly as long - as the call does.""" - lines = [" {"] + before.append( + f" Marshal.StructureToPtr({ident(name)}, " + f"{scratch(name)}, false);") + teardown.append(f" Marshal.FreeHGlobal({scratch(name)});") for name in method.scalar_arrays: - lines.append( + setup.append( f" GCHandle {scratch(name)} = " f"GCHandle.Alloc({ident(name)}, GCHandleType.Pinned);") - for name, element in method.arrays: - lines += [ - f" IntPtr[] {scratch(name)}Values = new IntPtr[{ident(name)}.Length];", + teardown.append(f" {scratch(name)}.Free();") + for name, _element in method.arrays: + setup += [ + f" IntPtr[] {scratch(name)}Values = " + f"new IntPtr[{ident(name)}.Length];", f" for (int i = 0; i < {ident(name)}.Length; i++)", " {", f" {scratch(name)}Values[i] = {ident(name)}[i].Ptr;", " }", "", - f" GCHandle {scratch(name)} = GCHandle.Alloc({scratch(name)}Values, GCHandleType.Pinned);", + f" GCHandle {scratch(name)} = " + f"GCHandle.Alloc({scratch(name)}Values, GCHandleType.Pinned);", ] - lines.append(" try") - lines.append(" {") - lines.append(f" return {method.body};") - lines.append(" }") - lines.append(" finally") - lines.append(" {") - for name in method.scalar_arrays: - lines.append(f" {scratch(name)}.Free();") - for name, _ in method.arrays: - lines.append(f" {scratch(name)}.Free();") - lines += [" }", " }"] + teardown.append(f" {scratch(name)}.Free();") + if method.length_out: + name = scratch(method.length_out) + setup.append(f" IntPtr {name} = Marshal.AllocHGlobal(sizeof(long));") + teardown.append(f" Marshal.FreeHGlobal({name});") + if method.out_param: + name, size, _reader = method.out_param + setup.append( + f" IntPtr {scratch(name)} = Marshal.AllocHGlobal({size});") + teardown.append(f" Marshal.FreeHGlobal({scratch(name)});") + + answer = self.answer(method) + if not teardown: + # Nothing was allocated, so there is nothing to free and no frame to + # free it in: the answer alone is the body. + return [" {"] + answer + [" }"] + lines = [" {"] + setup + [" try", " {"] + before + lines += [f" {line}" if line else "" for line in answer] + lines += [" }", " finally", " {"] + lines += teardown + [" }", " }"] return lines + def answer(self, method: Method) -> list[str]: + """What the body does with the call, once everything is in place.""" + if method.out_param: + # MEOS answers `false` where the value does not exist and leaves the + # out-parameter untouched, so the method answers null there. + _name, _size, reader = method.out_param + return [ + f" if (!{method.body})", + " {", + " return null;", + " }", + "", + f" return {reader};", + ] + if method.byte_buffer: + # A byte array does not carry its own length, so what MEOS wrote + # into the length buffer says how much of it to copy. + length = scratch(method.length_out) + return [ + f" IntPtr _bytes = {method.body};", + " if (_bytes == IntPtr.Zero)", + " {", + " return null;", + " }", + "", + f" byte[] _wkb = new byte[Marshal.ReadInt64({length})];", + " Marshal.Copy(_bytes, _wkb, 0, _wkb.Length);", + " return _wkb;", + ] + if "|>" in method.body: + # Several values answered together: the call is made once and each + # item read off what it answered. + answered, reads = method.body.split("|>", 1) + return [f" var _answered = {answered};", "", + f" return {reads};"] + return [f" return {method.body};"] + def to_string(self, cls: str, methods: list) -> list[str]: """`ToString` over the class's own text output, where MEOS publishes one. @@ -842,6 +849,30 @@ def convert_file(self) -> str: => Meos.DateIn( day.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)); + /// Each MEOS TimestampTz of an array, as a DateTime. + internal static DateTime[] ToDateTimeArray(long[] moments) + {{ + DateTime[] values = new DateTime[moments.Length]; + for (int i = 0; i < moments.Length; i++) + {{ + values[i] = ToDateTime(moments[i]); + }} + + return values; + }} + + /// Each MEOS DateADT of an array, as a DateOnly. + internal static DateOnly[] ToDateOnlyArray(int[] days) + {{ + DateOnly[] values = new DateOnly[days.Length]; + for (int i = 0; i < days.Length; i++) + {{ + values[i] = ToDateOnly(days[i]); + }} + + return values; + }} + /// The struct MEOS answers through a pointer, as a value. The /// memory behind the pointer stays MEOS's, as it does for every other /// value the layer reads back.