During the nightly survey of prqlc/bindings/dotnet/, I cross-checked the C# binding against the C header at prqlc/bindings/prqlc-c/prqlc.h and found several FFI/marshalling defects that look like they would prevent the binding from working correctly on 64-bit platforms or on any code path that returns more than one diagnostic message. Filing as an issue rather than a PR because the fix likely needs coordinated changes across several files plus a runnable .NET test environment to verify.
Defects found
-
Memory leak — result_destroy is never called. prqlc.h exports result_destroy(CompileResult *res) and the CompileResult doc comment says it "must be freed by result_destroy". PrqlCompiler.cs never invokes it on the path through Compile / PrqlToPl / PlToRq / RqToSql — every call leaks the native output string and the messages array. Fix: add the DllImport and call it in a finally (or implement IDisposable on Result).
-
size_t mapped as int (4 bytes) instead of nuint/UIntPtr (8 bytes on 64-bit). Affects:
On 64-bit platforms this corrupts the marshalled struct layout. MessagesLen in particular ends up reading the high or low half of a usize.
-
All messages collapse to the first one. Result.cs:20-23 calls Marshal.PtrToStructure<Message>(result.Messages) inside the loop without advancing the pointer, so every entry is a copy of the first message. Needs IntPtr.Add(result.Messages, i * Marshal.SizeOf<Message>()).
-
Message struct layout does not match the C struct. In prqlc.h the optional fields (code, hint, display) are const char *const * (pointer-to-pointer, may be null), and span / location are pointers (const Span *, const SourceLocation *). The C# Message struct marshals them as string and as inline by-value structs, which will read wrong memory and likely crash on any error path that populates them. Fields need to be IntPtr and dereferenced/null-checked manually.
-
Wrong exception type for null options. PrqlCompiler.cs:48 and :119 throw ArgumentException for null options, but the XML doc on lines 37 and 107 advertises ArgumentNullException. Either the doc or the throw is wrong.
-
Wrong paramref in RqToSql doc comments. PrqlCompiler.cs:106,108 reference <paramref name="prqlQuery"/> but the parameter is rqJson.
Context
The dotnet binding has had no commits in the last 6 months, and #4251 / #3791 raise the broader question of moving unmaintained bindings to separate repositories. If the binding is being kept in-tree, the four critical defects above (1–4) are blockers for any real use; #5 and #6 are cosmetic. If the binding is being deprioritized, this issue can serve as the "current known state" record before any move-out decision.
I have not built or run the .NET project to confirm a runtime crash — these findings are from reading the C# source against the C header.
During the nightly survey of
prqlc/bindings/dotnet/, I cross-checked the C# binding against the C header atprqlc/bindings/prqlc-c/prqlc.hand found several FFI/marshalling defects that look like they would prevent the binding from working correctly on 64-bit platforms or on any code path that returns more than one diagnostic message. Filing as an issue rather than a PR because the fix likely needs coordinated changes across several files plus a runnable .NET test environment to verify.Defects found
Memory leak —
result_destroyis never called.prqlc.hexportsresult_destroy(CompileResult *res)and theCompileResultdoc comment says it "must be freed byresult_destroy".PrqlCompiler.csnever invokes it on the path throughCompile/PrqlToPl/PlToRq/RqToSql— every call leaks the native output string and the messages array. Fix: add the DllImport and call it in afinally(or implementIDisposableonResult).size_tmapped asint(4 bytes) instead ofnuint/UIntPtr(8 bytes on 64-bit). Affects:NativeResult.cs:10—MessagesLenSpan.cs:15-20—Start/EndSourceLocation.cs:14-29—StartLine/StartCol/EndLine/EndColOn 64-bit platforms this corrupts the marshalled struct layout.
MessagesLenin particular ends up reading the high or low half of ausize.All messages collapse to the first one.
Result.cs:20-23callsMarshal.PtrToStructure<Message>(result.Messages)inside the loop without advancing the pointer, so every entry is a copy of the first message. NeedsIntPtr.Add(result.Messages, i * Marshal.SizeOf<Message>()).Messagestruct layout does not match the C struct. Inprqlc.hthe optional fields (code,hint,display) areconst char *const *(pointer-to-pointer, may be null), andspan/locationare pointers (const Span *,const SourceLocation *). The C#Messagestruct marshals them asstringand as inline by-value structs, which will read wrong memory and likely crash on any error path that populates them. Fields need to beIntPtrand dereferenced/null-checked manually.Wrong exception type for null
options.PrqlCompiler.cs:48and:119throwArgumentExceptionfor nulloptions, but the XML doc on lines 37 and 107 advertisesArgumentNullException. Either the doc or the throw is wrong.Wrong
paramrefinRqToSqldoc comments.PrqlCompiler.cs:106,108reference<paramref name="prqlQuery"/>but the parameter isrqJson.Context
The dotnet binding has had no commits in the last 6 months, and #4251 / #3791 raise the broader question of moving unmaintained bindings to separate repositories. If the binding is being kept in-tree, the four critical defects above (1–4) are blockers for any real use; #5 and #6 are cosmetic. If the binding is being deprioritized, this issue can serve as the "current known state" record before any move-out decision.
I have not built or run the .NET project to confirm a runtime crash — these findings are from reading the C# source against the C header.