-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
[API Proposal]: AsnWriter Encode with callback #75759
Comments
Tagging subscribers to this area: @dotnet/area-system-formats-asn1, @vcsjones Issue DetailsBackground and motivation
There are several uses of However, API Proposalnamespace System.Formats.Asn1;
public partial class AsnWriter {
public delegate TReturn MemoryEncodeFunc<TState, TReturn>(TState state, ReadOnlyMemory<byte> encoded);
public TReturn Encode<TState, TReturn>(TState state, MemoryEncodeFunc<TState, TReturn> encodeFunc);
} The proposal allows passing in a state and a return. The state and return can be used to feed in state and allow the callback to remain static and not capture anything. API UsageConsider this snippet in the runtime today: Lines 355 to 359 in b1f5e97
This would become: // In this example, the state parameter is not used.
return _writer.Encode((object)null, static (object state, ReadOnlyMemory<byte> encoded) => {
return new X500DistinguishedName(encoded.Span);
}); The same pattern is used for decoding DSA keys for macOS and Android: Line 95 in b1f5e97
Encrypting CMS in PKCS: Lines 144 to 146 in b1f5e97
Encoding a CMS: Line 136 in b1f5e97
Hashing signed attributes: Line 646 in b1f5e97
Etc. Alternative DesignsNo response RisksNo response
|
I feel like in my original proposal for AsnWriter I had a SpanAction (or custom SpanAction-like delegate) for Encode; but I don't see it in https://github.com/dotnet/designs/blob/main/accepted/2020/asnreader/asnreader.md or https://github.com/dotnet/corefxlab/blob/archive/src/System.Security.Cryptography.Asn1.Experimental/System/Security/Cryptography/Asn1/AsnWriter.cs so someone must have convinced me to cut it pretty early. (I also don't see it in the private implementation that led up to the public one... huh.) Anyways, the question from that is mainly why ReadOnlyMemory instead of ReadOnlySpan? I was going to say next that AsnWriter is pretty defensive about its buffer, but exposing the ReadOnlySpan in a SpanAction would still allow unsafe (or Unsafe) access to turn it back into something writable; so it probably doesn't matter that with ReadOnlyMemory it's slightly easier to just reach in and grab the array and turn it into something writable... Oh, there is one difference (aside from ReadOnlyMemory being able to flow to more places): As a ReadOnlyMemory someone can capture it and exit the callback with the reference. As a ReadOnlySpan it's tightly contained. I'll also ask if you think there's value in an Action-based variant, or if we should just make anyone who only cares about draining the value into (e.g.) a hash accumulator return 0 (until it becomes too popular to do so, or whatever). |
I think the original proposal had
Right now we have an internal API that just returns a
I started off this way, but changed it because a ROM is at least as useful as a ROS, but a ROS is not as useful as a ROM. For example, in the above examples I found of places this would be useful,
Right. Plus the capturing ergonomics you already mentioned.
I thought about it... but "just return |
I'm not thrilled about the idea of adding a delegate that exposes an ephemeral ReadOnlyMemory<byte> bytes = default;
instance.DoSomethingWith((/* ..., */ memory) => { bytes = memory; });
byte[] array = bytes.ToArray(); // ????? This code sample is basically unsafe-equivalent, because you could be pointing to something that has been returned to the array pool, or to random stackalloc memory in a frame which has already been popped, or <whatever>. Since we don't have a runtime / language-wide concept of borrowing an object, the best course of action is to rely on our existing type which represents "borrowed memory" - and that existing type is |
That's a good point that I agree with. Span makes sense from that perspective. Proposal updated. |
The delegate probably wants appropriate co-and-contra-variance annotations public delegate TReturn SpanEncodeFunc<in TState, out TReturn>(TState state, ReadOnlySpan<byte> encoded); For naming and ordering. Based on SpanAction, it probably should be
namespace System.Formats.Asn1
{
partial class AsnWriter
{
public delegate TReturn SpanEncodeFunc<in T, out TReturn>(ReadOnlySpan<byte> encoded, T arg);
public TReturn Encode<TState, TReturn>(TState state, SpanEncodeFunc<TState, TReturn> encodeFunc);
}
} There's also an interesting question of whether we should be doing a) SpanFunc, here Since we have a netstandard2.0 reference assembly, (c) is awkward, and (c) is not netstandard2.0, I'd suggest not (c). I feel like we don't like having public delegate types as nested types, so (a) is really (a') (System.Formats.Asn1.SpanEncodeFunc). Since nothing in the signature really ties it to the ASN.1 namespace that feels like it might be (b). // Do we have a place to do this ns2.0, or are we splitting our ref.cs now?
namespace System.Buffers
{
public delegate TReturn ReadOnlySpanFunc<in T, out TReturn>(ReadOnlySpan<byte> span, T arg);
}
namespace System.Formats.Asn1
{
partial class AsnWriter
{
public TReturn Encode<TState, TReturn>(TState state, SpanEncodeFunc<TState, TReturn> encodeFunc);
}
} @stephentoub We've avoided (RO)SpanFunc thus far... it it popped up with this proposal would that be something you'd endorse, fight, or not really care about? |
I'd like to see if we can get to a point in .NET 8 / C# 12 where ref struct constraints would enable us to define a more general set of delegates (cc: @AaronRobinsonMSFT, @steveharter), or better yet, enable us to use the existing In the meantime, we can make forward progress on these kinds of issues with whatever custom delegates we need to define, but it'd be good to keep track of the ones we'd like to rip out prior to shipping .NET 8 should we be able to replace them successfully. |
@stephentoub I assume you mean #65112? As in when |
Right, e.g. the goal here would be to be able to write: public TReturn Encode<TState, TReturn>(TState state, Func<ReadOnlySpan<byte>, TState, TReturn> encodeFunc) { ... } with the existing |
Updated the proposal with |
namespace System.Formats.Asn1;
public partial class AsnWriter {
#if NET9_0_OR_GREATER
public TReturn Encode<TReturn>(Func<ReadOnlySpan<byte>, TReturn> encodeCallback);
public TReturn Encode<TState, TReturn>(TState state, Func<TState, ReadOnlySpan<byte>, TReturn> encodeCallback)
where TState : allows ref struct;
#endif
} |
Background and motivation
AsnWriter
hasEncode
in two forms: writing to aSpan<byte>
and allocate-and-returnbyte[]
.There are several uses of
AsnWriter
where we write something to ASN.1 and then do something with it, like sign it or hash it. The allocating return well, allocates. The span-writing forces developers to come up with an intermediate, either on the stack, a pool rental, etc. Both require the contents to be copied.However,
AsnWriter
already has the data available internally. I propose we make this available with a callback toEncode
.API Proposal
The proposal allows passing in a state and a return. The state and return can be used to feed in state and allow the callback to remain static and not capture anything.
An overload without
state
exists so that passing in something is not required if it is not needed.Since System.Formats.Asn1 is supported down level, these APIs will only exist for .NET9+ since ref structs as type arguments is not permitted until then.
API Usage
Consider this snippet in the runtime today:
runtime/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X500DistinguishedNameBuilder.cs
Lines 355 to 359 in b1f5e97
This would become:
The same pattern is used for decoding DSA keys for macOS and Android:
runtime/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509Pal.macOS.cs
Line 95 in b1f5e97
Encrypting CMS in PKCS:
runtime/src/libraries/System.Security.Cryptography.Pkcs/src/Internal/Cryptography/Pal/AnyOS/ManagedPal.Encrypt.cs
Lines 144 to 146 in b1f5e97
Encoding a CMS:
runtime/src/libraries/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/SignedCms.cs
Line 136 in b1f5e97
Hashing signed attributes:
runtime/src/libraries/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/SignerInfo.cs
Line 646 in b1f5e97
Etc.
Alternative Designs
No response
Risks
No response
The text was updated successfully, but these errors were encountered: