-
Notifications
You must be signed in to change notification settings - Fork 617
Use ReadOnlyMemory<byte> for binary data to eliminate UTF-16 transcoding #1070
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
7dca602
4287694
8e6fcf0
e405dfc
ebe3eef
1d76c11
39213fb
a4ba4a9
b4e4eaf
61f0dfa
5de847c
dc01608
2fc35d9
14fedca
b3eacff
0a3015f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| #if !NET | ||
|
|
||
| namespace System.Text; | ||
|
|
||
| internal static class EncodingExtensions | ||
| { | ||
| /// <summary> | ||
| /// Gets the number of bytes required to encode the specified characters. | ||
| /// </summary> | ||
| public static int GetByteCount(this Encoding encoding, ReadOnlySpan<char> chars) | ||
| { | ||
| if (chars.IsEmpty) | ||
| { | ||
| return 0; | ||
| } | ||
|
|
||
| unsafe | ||
| { | ||
| fixed (char* charsPtr = chars) | ||
| { | ||
| return encoding.GetByteCount(charsPtr, chars.Length); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Encodes the specified characters into the specified byte span. | ||
| /// </summary> | ||
| public static int GetBytes(this Encoding encoding, ReadOnlySpan<char> chars, Span<byte> bytes) | ||
| { | ||
| if (chars.IsEmpty) | ||
| { | ||
| return 0; | ||
| } | ||
|
|
||
| unsafe | ||
| { | ||
| fixed (char* charsPtr = chars) | ||
| fixed (byte* bytesPtr = bytes) | ||
| { | ||
| return encoding.GetBytes(charsPtr, chars.Length, bytesPtr, bytes.Length); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #endif |
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -1,4 +1,7 @@ | ||||||||
| using System.Buffers; | ||||||||
| using System.Buffers.Text; | ||||||||
| using System.Diagnostics; | ||||||||
| using System.Runtime.InteropServices; | ||||||||
| using System.Text.Json.Serialization; | ||||||||
|
|
||||||||
| namespace ModelContextProtocol.Protocol; | ||||||||
|
|
@@ -9,8 +12,8 @@ namespace ModelContextProtocol.Protocol; | |||||||
| /// <remarks> | ||||||||
| /// <para> | ||||||||
| /// <see cref="BlobResourceContents"/> is used when binary data needs to be exchanged through | ||||||||
| /// the Model Context Protocol. The binary data is represented as a base64-encoded string | ||||||||
| /// in the <see cref="Blob"/> property. | ||||||||
| /// the Model Context Protocol. The binary data is represented as base64-encoded UTF-8 bytes | ||||||||
| /// in the <see cref="Blob"/> property, providing a zero-copy representation of the wire payload. | ||||||||
| /// </para> | ||||||||
| /// <para> | ||||||||
| /// This class inherits from <see cref="ResourceContents"/>, which also has a sibling implementation | ||||||||
|
|
@@ -24,18 +27,102 @@ namespace ModelContextProtocol.Protocol; | |||||||
| [DebuggerDisplay("{DebuggerDisplay,nq}")] | ||||||||
| public sealed class BlobResourceContents : ResourceContents | ||||||||
| { | ||||||||
| private ReadOnlyMemory<byte>? _decodedData; | ||||||||
| private ReadOnlyMemory<byte> _blob; | ||||||||
|
|
||||||||
| /// <summary> | ||||||||
| /// Creates an <see cref="BlobResourceContents"/> from raw data. | ||||||||
| /// </summary> | ||||||||
| /// <param name="data">The raw data.</param> | ||||||||
| /// <param name="uri">The URI of the data.</param> | ||||||||
| /// <param name="mimeType">The optional MIME type of the data.</param> | ||||||||
| /// <returns>A new <see cref="BlobResourceContents"/> instance.</returns> | ||||||||
| /// <exception cref="InvalidOperationException"></exception> | ||||||||
| public static BlobResourceContents FromData(ReadOnlyMemory<byte> data, string uri, string? mimeType = null) | ||||||||
| { | ||||||||
| ReadOnlyMemory<byte> blob; | ||||||||
|
|
||||||||
| // Encode directly to UTF-8 base64 bytes without string intermediate | ||||||||
| int maxLength = Base64.GetMaxEncodedToUtf8Length(data.Length); | ||||||||
| byte[] buffer = new byte[maxLength]; | ||||||||
| if (Base64.EncodeToUtf8(data.Span, buffer, out _, out int bytesWritten) == OperationStatus.Done) | ||||||||
| { | ||||||||
stephentoub marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
| Debug.Assert(bytesWritten == buffer.Length, "Base64 encoding should always produce exact length for non-padded input"); | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
| blob = buffer.AsMemory(0, bytesWritten); | ||||||||
| } | ||||||||
| else | ||||||||
| { | ||||||||
| throw new InvalidOperationException("Failed to encode binary data to base64"); | ||||||||
| } | ||||||||
|
|
||||||||
| return new() | ||||||||
| { | ||||||||
| _decodedData = data, | ||||||||
| Blob = blob, | ||||||||
| MimeType = mimeType, | ||||||||
| Uri = uri | ||||||||
| }; | ||||||||
| } | ||||||||
|
|
||||||||
| /// <summary> | ||||||||
| /// Gets or sets the base64-encoded string representing the binary data of the item. | ||||||||
| /// Gets or sets the base64-encoded UTF-8 bytes representing the binary data of the item. | ||||||||
| /// </summary> | ||||||||
| /// <remarks> | ||||||||
| /// This is a zero-copy representation of the wire payload of this item. Setting this value will invalidate any cached value of <see cref="DecodedData"/>. | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The "zero-copy representation of the wire payload" part of this is confusing. |
||||||||
| /// </remarks> | ||||||||
| [JsonPropertyName("blob")] | ||||||||
| public required string Blob { get; set; } | ||||||||
| public required ReadOnlyMemory<byte> Blob | ||||||||
| { | ||||||||
| get => _blob; | ||||||||
| set | ||||||||
| { | ||||||||
| _blob = value; | ||||||||
| _decodedData = null; // Invalidate cache | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| /// <summary> | ||||||||
| /// Gets or sets the decoded data represented by <see cref="Blob"/>. | ||||||||
| /// </summary> | ||||||||
| /// <remarks> | ||||||||
| /// <para> | ||||||||
| /// When getting, this member will decode the value in <see cref="Blob"/> and cache the result. | ||||||||
| /// Subsequent accesses return the cached value unless <see cref="Blob"/> is modified. | ||||||||
| /// </para> | ||||||||
| /// <para> | ||||||||
| /// When setting, the binary data is stored without copying and <see cref="Blob"/> is updated | ||||||||
| /// with the base64-encoded UTF-8 representation. | ||||||||
| /// </para> | ||||||||
| /// </remarks> | ||||||||
| [JsonIgnore] | ||||||||
| public ReadOnlyMemory<byte> DecodedData | ||||||||
| { | ||||||||
| get | ||||||||
| { | ||||||||
| if (_decodedData is null) | ||||||||
| { | ||||||||
| // Decode directly from UTF-8 base64 bytes without string intermediate | ||||||||
| int maxLength = Base64.GetMaxDecodedFromUtf8Length(Blob.Length); | ||||||||
| byte[] buffer = new byte[maxLength]; | ||||||||
| if (Base64.DecodeFromUtf8(Blob.Span, buffer, out _, out int bytesWritten) == System.Buffers.OperationStatus.Done) | ||||||||
| { | ||||||||
| _decodedData = buffer.AsMemory(0, bytesWritten); | ||||||||
| } | ||||||||
| else | ||||||||
| { | ||||||||
| throw new FormatException("Invalid base64 data"); | ||||||||
| } | ||||||||
| } | ||||||||
| return _decodedData.Value; | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| [DebuggerBrowsable(DebuggerBrowsableState.Never)] | ||||||||
| private string DebuggerDisplay | ||||||||
| { | ||||||||
| get | ||||||||
| { | ||||||||
| string lengthDisplay = DebuggerDisplayHelper.GetBase64LengthDisplay(Blob); | ||||||||
| string lengthDisplay = _decodedData is null ? DebuggerDisplayHelper.GetBase64LengthDisplay(Blob) : $"{DecodedData.Length} bytes"; | ||||||||
| string mimeInfo = MimeType is not null ? $", MimeType = {MimeType}" : ""; | ||||||||
| return $"Uri = \"{Uri}\"{mimeInfo}, Length = {lengthDisplay}"; | ||||||||
| } | ||||||||
|
|
||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's still copied. It just doesn't have its encoding changed.