fix: correct .NET binding FFI marshalling and free native results#5848
fix: correct .NET binding FFI marshalling and free native results#5848
Conversation
Several FFI defects in `prqlc/bindings/dotnet/` prevented the binding from working correctly on 64-bit platforms or returning more than one diagnostic (reported in #5847). This change: - Maps `size_t` as `UIntPtr` instead of `int` for `messages_len`, `Span`, and `SourceLocation` so the marshalled struct layout matches the C ABI. - Iterates the `messages` array by advancing the `IntPtr` (was reading the same first entry every iteration). - Dereferences the indirect `code` / `hint` / `display` pointer-to-pointer fields and the `span` / `location` pointers manually instead of asking the marshaller to inline them by value. - Calls `result_destroy` from `Result`'s constructor so native memory is not leaked. `NativeResult.Output` is now `IntPtr` so the original native pointer is retained for the destroy call; the string is decoded as UTF-8 before being returned through the public API. - Tightens the exception type to `ArgumentNullException` for null `options` (the XML doc already documented it) and fixes the stale `<paramref>` references in `RqToSql`. `prqlc-c::result_destroy` previously hit `Vec::from_raw_parts(null, 0, 0)` on the success path, which is undefined behaviour and panics under modern Rust's debug checks. Guarding the messages free (and the output free) with a null check makes the destructor safe to call after a successful compile, which the .NET binding now does. Adds tests covering the error path (Span / Location / Display all populated through indirect pointers) and the corrected `ArgumentNullException` behaviour. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
Hey @prql-bot this is great, do you think could upgrade it to .NET 10? If so, do you think you could change |
|
Thanks @vanillajonathan — opened #5850 with the modernization. It bumps the binding to The main trade-off is dropping the existing |
Fixes #5847.
The .NET binding had several FFI defects that prevented it from working correctly on 64-bit platforms or returning more than one diagnostic message. This PR addresses defects 1–6 from the issue plus a related null-pointer bug in
prqlc-c::result_destroythat surfaced once the binding actually started calling it.Changes to the .NET binding
result_destroynever called — every compile leaked the output string and messages array.Result's constructor now decodes everything from the native struct, then callsresult_destroyin afinally.NativeResult.OutputisIntPtrso the original native pointer is retained for the destroy call; UTF-8 decoding happens in C#.size_tmapped asint(4 bytes) instead ofUIntPtr(8 bytes on 64-bit) — corrupted struct layout.MessagesLen,Span.Start/End, andSourceLocation.StartLine/StartCol/EndLine/EndColnow useUIntPtrfor marshalling. PublicSpan/SourceLocationexpose the values asulong.IntPtr.Add(result.Messages, i * Marshal.SizeOf<NativeMessage>()).Messagestruct layout did not match the C struct — optional fields areconst char *const *(pointer-to-pointer) andspan/locationareconst Span */const SourceLocation *.NativeMessageusesIntPtrfor every pointer field; a new publicMessageclass exposes nullable string properties andSpan?/SourceLocation?. Indirect string fields are dereferenced once before reading the C string.ArgumentExceptionthrown for nulloptions, but XML doc advertisedArgumentNullException.ArgumentNullException.RqToSqldoc comments referenced<paramref name="prqlQuery"/>instead ofrqJson.Related fix to
prqlc-c::result_destroyOn the success path
result_into_c_strsetsmessages: ::std::ptr::null_mut()andmessages_len: 0. The originalresult_destroythen calledVec::from_raw_parts(null, 0, 0), which violatesNonNull::new_unchecked's precondition and aborts under modern Rust's debug check (unsafe precondition(s) violated: NonNull::new_unchecked requires that the pointer is non-null). The destructor now skips the messages free and the output free when those pointers are null, so callingresult_destroyafter a successful compile is safe.This was previously latent — the existing C/C++ examples called
result_destroyafter a clean compile too, but builtprqlc-cin release mode where the precondition check is elided. The .NET CI builds in debug mode, which is how the bug surfaced.Tests
Span,Location, andDisplayare all populated, exercising every indirect-pointer field at once.ArgumentNullExceptionassertions forCompileandRqToSqlwhenoptionsis null.Note on API breakage
Span,SourceLocation, andMessageare exposed publicly and their shapes change (struct → class forMessage; field types frominttoulongfor the size_t cases; nullable wrappers for optional fields). The package is still at0.1.0and is not published to NuGet, so this is acceptable.