Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@

**Integrations**:

- Fix several FFI defects in the .NET binding that prevented it from working
correctly on 64-bit platforms or returning more than one diagnostic: map
`size_t` as `UIntPtr` (was `int`), advance the pointer when iterating the
messages array, dereference indirect string fields and `Span` / `Location`
pointers, call `result_destroy` to free native memory, and tighten exception
types and doc references. Also guard `result_destroy` in `prqlc-c` against a
null `messages` pointer, which is set on the success path. (@prql-bot, #5847)

**Internal changes**:

**New Contributors**:
Expand Down
34 changes: 34 additions & 0 deletions prqlc/bindings/dotnet/PrqlCompiler.Tests/CompilerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,40 @@ public void ToCompile_Works()
Assert.Equal(expected, result.Output);
}

[Fact]
public void Compile_ReportsErrorMessages()
{
// Arrange — `unknown_function` is not defined, producing an error
// message whose optional fields (Span, Display, Location) get
// populated. This validates pointer dereferencing for indirect
// string fields and Span/Location pointers in the FFI struct layout.
var query = "from employees | unknown_function col";

// Act
var result = PrqlCompiler.Compile(query);

// Assert
Assert.NotEmpty(result.Messages);
var message = result.Messages.First();
Assert.Equal(MessageKind.Error, message.Kind);
Assert.False(string.IsNullOrEmpty(message.Reason));
Assert.NotNull(message.Span);
Assert.NotNull(message.Location);
Assert.False(string.IsNullOrEmpty(message.Display));
}

[Fact]
public void Compile_ThrowsArgumentNullException_WhenOptionsNull()
{
Assert.Throws<ArgumentNullException>(() => PrqlCompiler.Compile("from x", null!));
}

[Fact]
public void RqToSql_ThrowsArgumentNullException_WhenOptionsNull()
{
Assert.Throws<ArgumentNullException>(() => PrqlCompiler.RqToSql("{}", null!));
}

[Fact]
public void TestOtherFunctions()
{
Expand Down
2 changes: 2 additions & 0 deletions prqlc/bindings/dotnet/PrqlCompiler.Tests/Usings.cs
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
global using System;
global using System.Linq;
global using Xunit;
19 changes: 8 additions & 11 deletions prqlc/bindings/dotnet/PrqlCompiler/Message.cs
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
using System.Runtime.InteropServices;

namespace Prql.Compiler
{
/// <summary>
/// Compile result message.
/// </summary>
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct Message
public class Message
{
/// <summary>
/// Message kind. Currently only Error is implemented.
/// </summary>
public MessageKind Kind { get; set; }

/// <summary>
/// Machine-readable identifier of the error.
/// Machine-readable identifier of the error. May be null.
/// </summary>
public string Code { get; set; }

Expand All @@ -24,23 +21,23 @@ public struct Message
public string Reason { get; set; }

/// <summary>
/// A list of suggestions of how to fix the error.
/// A suggestion of how to fix the error. May be null.
/// </summary>
public string Hint { get; set; }

/// <summary>
/// Character offset of error origin within a source file.
/// Character offset of error origin within a source file. May be null.
/// </summary>
public Span Span { get; set; }
public Span? Span { get; set; }

/// <summary>
/// Annotated code, containing cause and hints.
/// Annotated code, containing cause and hints. May be null.
/// </summary>
public string Display { get; set; }

/// <summary>
/// Line and column number of error origin within a source file.
/// Line and column number of error origin within a source file. May be null.
/// </summary>
public SourceLocation Location { get; set; }
public SourceLocation? Location { get; set; }
}
}
17 changes: 17 additions & 0 deletions prqlc/bindings/dotnet/PrqlCompiler/NativeMessage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System;
using System.Runtime.InteropServices;

namespace Prql.Compiler
{
[StructLayout(LayoutKind.Sequential)]
internal struct NativeMessage
{
public MessageKind Kind;
public IntPtr Code;
public IntPtr Reason;
public IntPtr Hint;
public IntPtr Span;
public IntPtr Display;
public IntPtr Location;
}
}
6 changes: 4 additions & 2 deletions prqlc/bindings/dotnet/PrqlCompiler/NativeResult.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
using System;
using System.Runtime.InteropServices;

namespace Prql.Compiler
{
[StructLayout(LayoutKind.Sequential)]
internal struct NativeResult
{
#pragma warning disable CS0649 // Field is never assigned to
public string Output;
public IntPtr Output;
public IntPtr Messages;
public int MessagesLen;
public UIntPtr MessagesLen;
#pragma warning restore CS0649 // Field is never assigned to
}
}
14 changes: 14 additions & 0 deletions prqlc/bindings/dotnet/PrqlCompiler/NativeSourceLocation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System;
using System.Runtime.InteropServices;

namespace Prql.Compiler
{
[StructLayout(LayoutKind.Sequential)]
internal struct NativeSourceLocation
{
public UIntPtr StartLine;
public UIntPtr StartCol;
public UIntPtr EndLine;
public UIntPtr EndCol;
}
}
12 changes: 12 additions & 0 deletions prqlc/bindings/dotnet/PrqlCompiler/NativeSpan.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System;
using System.Runtime.InteropServices;

namespace Prql.Compiler
{
[StructLayout(LayoutKind.Sequential)]
internal struct NativeSpan
{
public UIntPtr Start;
public UIntPtr End;
}
}
8 changes: 4 additions & 4 deletions prqlc/bindings/dotnet/PrqlCompiler/PrqlCompiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public static Result Compile(string prqlQuery, PrqlCompilerOptions options)

if (options is null)
{
throw new ArgumentException(nameof(options));
throw new ArgumentNullException(nameof(options));
}

var nativeOptions = new NativePrqlCompilerOptions(options);
Expand Down Expand Up @@ -103,9 +103,9 @@ public static Result PlToRq(string plJson)
/// <param name="rqJson">RQ string in JSON format.</param>
/// <param name="options">PRQL compiler options.</param>
/// <returns>JSON.</returns>
/// <exception cref="ArgumentException"><paramref name="prqlQuery"/> is null or empty.</exception>
/// <exception cref="ArgumentException"><paramref name="rqJson"/> is null or empty.</exception>
/// <exception cref="ArgumentNullException"><paramref name="options"/> is <c>null</c>.</exception>
/// <exception cref="FormatException"><paramref name="prqlQuery"/> cannot be compiled.</exception>
/// <exception cref="FormatException"><paramref name="rqJson"/> cannot be compiled.</exception>
/// <remarks>https://docs.rs/prqlc/latest/</remarks>
public static Result RqToSql(string rqJson, PrqlCompilerOptions options)
{
Expand All @@ -116,7 +116,7 @@ public static Result RqToSql(string rqJson, PrqlCompilerOptions options)

if (options is null)
{
throw new ArgumentException(nameof(options));
throw new ArgumentNullException(nameof(options));
}

var nativeOptions = new NativePrqlCompilerOptions(options);
Expand Down
92 changes: 85 additions & 7 deletions prqlc/bindings/dotnet/PrqlCompiler/Result.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;

namespace Prql.Compiler
{
Expand All @@ -13,16 +14,27 @@ public class Result

internal Result(NativeResult result)
{
Output = result.Output;
try
{
Output = PtrToUtf8String(result.Output) ?? string.Empty;

var len = checked((int)result.MessagesLen.ToUInt64());
var messages = new List<Message>(len);
var nativeMessageSize = Marshal.SizeOf<NativeMessage>();

var messages = new List<Message>();
for (var i = 0; i < len; i++)
{
var entryPtr = IntPtr.Add(result.Messages, i * nativeMessageSize);
var native = Marshal.PtrToStructure<NativeMessage>(entryPtr);
messages.Add(ConvertMessage(native));
}

for (var i = 0; i < result.MessagesLen; i++)
_messages = messages.AsReadOnly();
}
finally
{
messages.Add(Marshal.PtrToStructure<Message>(result.Messages));
ResultDestroyExtern(result);
}

_messages = messages.ToList().AsReadOnly();
}

/// <summary>
Expand All @@ -34,5 +46,71 @@ internal Result(NativeResult result)
/// Error, warning and lint messages.
/// </summary>
public IReadOnlyCollection<Message> Messages => _messages;

private static Message ConvertMessage(NativeMessage native)
{
return new Message
{
Kind = native.Kind,
Code = PtrToUtf8StringIndirect(native.Code),
Reason = PtrToUtf8String(native.Reason) ?? string.Empty,
Hint = PtrToUtf8StringIndirect(native.Hint),
Span = ReadStruct<NativeSpan>(native.Span) is NativeSpan s
? new Span { Start = s.Start.ToUInt64(), End = s.End.ToUInt64() }
: (Span?)null,
Display = PtrToUtf8StringIndirect(native.Display),
Location = ReadStruct<NativeSourceLocation>(native.Location) is NativeSourceLocation l
? new SourceLocation
{
StartLine = l.StartLine.ToUInt64(),
StartCol = l.StartCol.ToUInt64(),
EndLine = l.EndLine.ToUInt64(),
EndCol = l.EndCol.ToUInt64(),
}
: (SourceLocation?)null,
};
}

private static T? ReadStruct<T>(IntPtr ptr) where T : struct
{
if (ptr == IntPtr.Zero)
{
return null;
}
return Marshal.PtrToStructure<T>(ptr);
}

private static string PtrToUtf8StringIndirect(IntPtr pointerToPointer)
{
if (pointerToPointer == IntPtr.Zero)
{
return null;
}
var stringPtr = Marshal.ReadIntPtr(pointerToPointer);
return PtrToUtf8String(stringPtr);
}

private static string PtrToUtf8String(IntPtr ptr)
{
if (ptr == IntPtr.Zero)
{
return null;
}
var len = 0;
while (Marshal.ReadByte(ptr, len) != 0)
{
len++;
}
if (len == 0)
{
return string.Empty;
}
var bytes = new byte[len];
Marshal.Copy(ptr, bytes, 0, len);
return Encoding.UTF8.GetString(bytes);
}

[DllImport("libprqlc_c", EntryPoint = "result_destroy")]
private static extern void ResultDestroyExtern(NativeResult res);
}
}
11 changes: 4 additions & 7 deletions prqlc/bindings/dotnet/PrqlCompiler/SourceLocation.cs
Original file line number Diff line number Diff line change
@@ -1,31 +1,28 @@
using System.Runtime.InteropServices;

namespace Prql.Compiler
{
/// <summary>
/// Location within a source file.
/// </summary>
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct SourceLocation
{
/// <summary>
/// Start line.
/// </summary>
public int StartLine { get; set; }
public ulong StartLine { get; set; }

/// <summary>
/// Start column.
/// </summary>
public int StartCol { get; set; }
public ulong StartCol { get; set; }

/// <summary>
/// End line.
/// </summary>
public int EndLine { get; set; }
public ulong EndLine { get; set; }

/// <summary>
/// End column.
/// </summary>
public int EndCol { get; set; }
public ulong EndCol { get; set; }
}
}
7 changes: 2 additions & 5 deletions prqlc/bindings/dotnet/PrqlCompiler/Span.cs
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
using System.Runtime.InteropServices;

namespace Prql.Compiler
{
/// <summary>
/// Identifier of a location in source.
/// Contains offsets in terms of chars.
/// </summary>
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct Span
{
/// <summary>
/// Start offset.
/// </summary>
public int Start { get; set; }
public ulong Start { get; set; }

/// <summary>
/// End offset.
/// </summary>
public int End { get; set; }
public ulong End { get; set; }
}
}
Loading
Loading