Skip to content
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

Unsafe.AsRef cannot be used with ref structs #68709

Closed
Neme12 opened this issue Apr 29, 2022 · 8 comments
Closed

Unsafe.AsRef cannot be used with ref structs #68709

Neme12 opened this issue Apr 29, 2022 · 8 comments

Comments

@Neme12
Copy link

Neme12 commented Apr 29, 2022

string Foo(in DefaultInterpolatedStringHandler handler)
{
    return string.Create(CultureInfo.InvariantCulture, ref Unsafe.AsRef(in handler));
}

DefaultInterpolatedStringHandler cannot be used as a type argument to Unsafe.AsRef because it is a ref struct. Is there any workaround for this or any other way to achieve this?

@dotnet-issue-labeler dotnet-issue-labeler bot added area-System.Runtime.CompilerServices untriaged New issue has not been triaged by the area owner labels Apr 29, 2022
@ghost
Copy link

ghost commented Apr 29, 2022

Tagging subscribers to this area: @dotnet/area-system-runtime-compilerservices
See info in area-owners.md if you want to be subscribed.

Issue Details
void Foo(in DefaultInterpolatedStringHandler handler)
{
    return string.Create(CultureInfo.InvariantCulture, ref Unsafe.AsRef(in handler));
}

DefaultInterpolatedStringHandler cannot be used as a type argument to Unsafe.AsRef because it is a ref struct. Is there any workaround for this or any other way to do this?

Author: Neme12
Assignees: -
Labels:

area-System.Runtime.CompilerServices, untriaged

Milestone: -

@teo-tsirpanis
Copy link
Contributor

Hello @Neme12, there is no safe workaround; the Foo method must accept the handler with ref. The handler is a mutable struct, which means that passing it with in might cause defensive copies, which is not good for performance.

@Neme12
Copy link
Author

Neme12 commented Apr 29, 2022

@teo-tsirpanis But the defensive copy would only happen when calling a non-readonly method on handler, not immediately when it is passed as an in parameter, right? So if Unsafe.AsRef worked here, I think this should be OK. Also, I know there is no safe workaround, that's why I'm trying to use Unsafe in the first place 😄

@Neme12
Copy link
Author

Neme12 commented Apr 29, 2022

BTW I need to use in instead of ref in the parameter because the method is actually an operator method, and operators cannot take ref parameters, only in.

@huoyaoyuan
Copy link
Member

The handler is a mutable struct, which means that passing it with in might cause defensive copies, which is not good for performance.

In fact, the handler must be passed by ref and must not be copied. It must be mutated to use it correctly. string.Create mutates it.

@Neme12
Copy link
Author

Neme12 commented Apr 30, 2022

@huoyaoyuan
I know. But as far as I know, it would only get copied if I invoked a (non-readonly) method/property on handler, it doesn't get copied just because it's passed as in (after all, the whole purpose of the in feature is to avoid copies). In my sample, I don't invoke any method on it, so it doesn't get copied. If Unsafe.AsRef worked with ref structs, this sample would work just fine without any copies at all, because Unsafe.AsRef takes it as an in parameter and returns a ref.

@DaZombieKiller
Copy link
Contributor

DaZombieKiller commented May 1, 2022

The problem here is a more fundamental limitation: ref structs cannot be used as generic arguments currently (because it would allow them to be boxed, and there is no existing feature to disallow a generic argument being boxed).

There is an issue tracking the status on support for using them in generics here: #65112.

I would not actually recommend using this, but you can work around the limitation currently by using this awful hack:

unsafe string Foo(in DefaultInterpolatedStringHandler handler)
{
    var asRef = (delegate*<in DefaultInterpolatedStringHandler, ref DefaultInterpolatedStringHandler>)(delegate*<in byte, ref byte>)&Unsafe.AsRef<byte>;
    return string.Create(CultureInfo.InvariantCulture, ref asRef(in handler));
}

However, the use case presented here is rather questionable based on your earlier description. Do you have more information about what you're doing? string.Create takes a ref because it intends to mutate the value, by taking an in parameter you are presenting an API contract that you immediately violate (regardless of intention.)

@Neme12
Copy link
Author

Neme12 commented May 2, 2022

@DaZombieKiller Wow, thanks for the workaround! I'm surprised that you can just convert the function to a different one and it works even though byte has a different size than DefaultInterpolatedStringHandler. I guess it's actually fine because Unsafe.AsRef doesn't actually store the value anywhere on the stack and just manipulates a reference, right?

I know that not being able to use ref structs as type arguments is the limitation. I was looking either for a workaround that would let me do what Unsafe.AsRef does in some other way without using the method, or by somehow circumventing the limitation and calling the method in some hacky way anyway, just like this.

However, the use case presented here is rather questionable based on your earlier description. Do you have more information about what you're doing? string.Create takes a ref because it intends to mutate the value, by taking an in parameter you are presenting an API contract that you immediately violate (regardless of intention.)

Yes, I know it's hacky and it's an API contract that I violate. I would use a ref parameter instead of in if I could, but the method is actually an operator and operators cannot have ref parameters 😔 (they can have in parameters though). But I don't think this is actually a problem in practice because this will always be used only by passing a interpolated string literal as the parameter, not a DefaultInterpolatedStringHandler that you'll create on your own. I mean, that type isn't meant to be instantiated by user code anyway, it's just for the compiler. A consumer of this API will know to just pass an interpolated string there and everything will work, so there's no issue there because they will never have a variable of type DefaultInterpolatedStringHandler.

One thing I actually forgot about and didn't account for is that when the method receives a DefaultInterpolatedStringHandler, either as in or as ref, the string is already built with the current culture by the compiler, so calling string.Create(CultureInfo.InvariantCulture, ref handler) will have no effect on changing the culture - the culture has to be passed in to the constructor of the type, which the compiler does based on InterpolatedStringHandlerArgumentAttribute. To make this work, I would have to add an additional provider parameter to the method, but I don't want the user to provide the culture - I want to hardcode it to CultureInfo.InvariantCulture, so what I actually had to do was to create my own handler (InvariantInterpolatedStringHandler) that wraps around DefaultInterpolatedStringHandler except that it provides the culture in the constructor. I still needed your workaround though (to be able to call ToStringAndClear without making a copy) because it still has to be a ref struct. Thanks!

@Neme12 Neme12 closed this as completed May 2, 2022
@ghost ghost removed the untriaged New issue has not been triaged by the area owner label May 2, 2022
@ghost ghost locked as resolved and limited conversation to collaborators Jun 1, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

4 participants