-
Notifications
You must be signed in to change notification settings - Fork 95
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
Friendly overload should expose [In, Out] PWSTR
in more friendly way
#144
Comments
Would you agree that Used without private (string Path, int Index) PathParseIconLocation(string pathAndIndex)
{
const int MAX_PATH = 260;
var szIconFile = new char[MAX_PATH];
pathAndIndex.CopyTo(szIconFile, 0);
int index = PInvoke.PathParseIconLocation(szIconFile);
return (new string(szIconFile), index);
}
My second choice if |
Put all that gunk in the wrapper. I should be able to just pass in a StringBuilder and call it a day. Why does everyone have to do this extra work? And why is everyone so focused on removing an alloc and not the ancillary effects like increasing expended cpu cycles or image size? |
So where the native code takes a pointer and a length we can and do take |
None of these alternatives to Image size would not increase appreciably either. I suspect the reason @jnm2 is still suggesting alternatives to |
Does I see StringBuilder as extra work. Usually it feels like I'm doing more work and writing more code than I really want. Unless I have at least two CopyTo calls, which is rare, StringBuilder always feels like it's less user friendly to me. It would be a plus if the friendly overload was also capable of avoiding allocations if desired, not just the pointer overload, but that's not the driving factor for me. The driving factor for me is that StringBuilder is not as nice to use as |
@jnm2 FYI: this is legal C# and would work if we offered a private (string Path, int Index) PathParseIconLocation(string pathAndIndex)
{
const int MAX_PATH = 260;
Span<char> szIconFile = stackalloc char[MAX_PATH];
pathAndIndex.AsSpan().CopyTo(szIconFile);
int index = PInvoke.PathParseIconLocation(szIconFile);
return (new string(szIconFile), index);
} |
@AArnott Yes, I picked up that you said that. I like that. |
I agree. In the
You can read about I need to research however whether the string it represents is always contiguous when I get a pointer to it (some evidence suggests that is not the case) or whether the content when initialized by managed code is always null terminated. |
To be clear, .NET supports marshaling |
Just found this doc which says:
It elaborates further if you care to read about it. It pitches use of |
[In, Out] PWSTR
as StringBuilder
[In, Out] PWSTR
in more friendly way
That page doesn't answer my question. What happens if the text you append exceeds the initial capacity? Then there might be no zero terminator within the buffer length that the API expects. Is that not a pitfall that puts StringBuilder on more even footing with |
I know it doesn't answer your question. I already stated that I need to research...
|
And anyway, as the |
Ah, I didn't recognize your comment about contiguity as being in reference to my question. Sorry! |
If we accept |
I wonder what the marshaler does. My preference would be to throw rather than truncate the string, since the consequences of proceeding with an unintentionally truncated string (especally with I/O) could be important and maybe even exploitable. |
When/why would we ever truncate the string? I wasn't suggesting that. I was saying that if we accept a |
Can you change your goal instead? Why is that your goal? If you consider that off-topic then I can open a separate bug for general project documentation. |
@AArnott In an API like this, where there's a documented maximum length: const int MAX_PATH = 260;
var szIconFile = new StringBuilder(pathAndIndex, MAX_PATH);
int index = PInvoke.PathParseIconLocation(szIconFile);
return (szIconFile.ToString(), index); What happens if Maybe the API detects this and errors. But then there is no concern that the use of I'm still trying to understand how StringBuilder succeeds where |
@riverar One |
@AArnott I'm not convinced at all, but rather than be an impediment I'll kindly step back and watch from afar. Thanks for your patience.
Not sure what this means. It's a |
I think I see what I'm missing. It's not that the API wouldn't check, it's that there might be additional characters and then a zero terminator immediately following the span, so the API can't know that this is not what you meant to send. The marshaling impl could solve this for |
@riverar Andrew doesn't think MSIHANDLE needs to be IntPtr sized. .NET thinks it does, if you use SafeHandle to wrap the handle. Rather than missing out on the benefits of SafeHandle encapsulation, Andrew is passing the wrapped handle value as 32 bits since .NET won't. |
I don't know. That would be a per-API behavior and outside the scope of a general policy for generating overloads. Folks need to read the docs for these APIs and do the right thing.
We must not rely an that. We know nothing about the bytes after the last While we can certainly write it off as user error if they do not null-terminate, I think looking at the last
It doesn't. |
Certain handles are documented to never use the high 32-bits, but this cannot be assumed to be true for all handles. Just to warn you. |
That's one possible end. Another is that it encounters an AV before it encounters a null character and crashes your app. |
Yes, that's what I'm proposing. |
@tannergooding That is almost true. What about the wine case? I wonder what we should do to run properly there. Also the calling convention used in C# 9 function pointers left me wondering how we would work there. It certainly isn't a top concern or supported. But it makes me wonder. |
I'd imagine that's a case where WINE itself needs to be concerned with converting between |
Definitely a gotcha when trying to use these APIs, if you're a C# developer, you just start hitting a wall. Most of the old examples findable use Would be nice to see some documentation and help to guide folks down the right path and be able to have different overloads which can use a simple It is weird when the official docs have a nice sample here, but then say not to do that here. Just want to point out when it's faster/easier to copy two lines of code to do the old PInvoke with a |
Coming here a year later, has there been any progress on this in the interim? Like @michael-hawker alluded to, I'm using cswin32 for all pinvokes in my current project except for GetWindowText, as the manual pinvoke is just so much more readable |
Not on this issue. I've been working on some other projects, but recently spent a bit more time on CsWin32. Not sure when I'll get to this, TBH. I hope soon. |
The friendly overloads in the issue description are obviously hand-written and contain return types that would not be generally creatable by CsWin32 from their native prototypes. // demonstrates calling pattern for the new friendly overload:
[Fact]
public void PathParseIconLocation_Friendly()
{
string sourceString = "hi there,3";
Span<char> buffer = new char[PInvoke.MAX_PATH + 1];
sourceString.AsSpan().CopyTo(buffer);
int result = PathParseIconLocation(ref buffer);
Assert.Equal(3, result);
Assert.Equal("hi there", buffer.ToString());
}
// proposed helper method:
private static unsafe int PathParseIconLocation(ref Span<char> buffer)
{
fixed (char* pBuffer = buffer)
{
PWSTR wstr = pBuffer;
int result = PInvoke.PathParseIconLocation(wstr);
buffer = buffer.Slice(0, wstr.Length);
return result;
}
} |
This issue was to cover the lack of a friendly way to call this API. I maintain that using a bunch of unsafe unidiomatic code with memory pinning is not friendly in any way, shape, or form. |
There is no unsafe code or memory pinning in the |
@AArnott Oh I misunderstood that. This is great then. |
@AArnott this code looks incorrect. The native signatures is Further, this function in particular requires that a buffer of |
In general, there are many native APIs in this format and even the C headers do not provide enough context (even in the SAL annotations) to be able to automatically determine the "correct" handling in every scenario. There are going to be many potential CVEs and other issues for APIs like this where the "safe" signature is detected via some heuristic. |
Good catch, missed that. Am a little surprised CI didn't catch this, are those manual tests? (Not familiar with this project.) |
I see that in the docs, but it doesn't quite make sense. And looking at the impl., it doesn't appear this is true (anymore?). Suspect this is a doc bug. |
The The null-terminator being required is definitely still the case (there is no length passed in) as is the more general point of potential CVEs if these "helper signatures" aren't carefully reviewed and considered. |
Thanks for reviewing, @tannergooding.
Well, that's a bug in my example rather than a bug in the generated code. But I should still fix that. It'll be considerably more complicated to do it 'right' which undermines the simplicity of the use case, sadly.
That's why the helper method just takes a I wonder though, to be more defensive around easy bugs like the one I made in the caller, should the generated code ensure that the span contains a null character somewhere? We could first check the last character to see if it's null, and only search the whole span if null isn't at the last position. The goal being to ensure that the PWSTR contract of a null terminator is honored somewhere in the buffer. That penalizes runtime for those who get it right, but it will probably not be significant. And if it is for some, well those folks can always go back to calling the
I wonder if CsWin32 should inject an assembly-level attribute to effectively document the version of CsWin32 that was used in a particular compilation. That way if memory bugs are found in CsWin32-generated code, we can more easily identify possibly impacted assemblies. |
@tannergooding So, possible doc bug aside, when an API says "a null-terminated string of length MAX_PATH", does that mean the buffer must be |
I think there are lots of options and there has been discussion (on the Runtime team) on whether there could be some analyzer to help flag problematic cases around In general, however, this is a large potential pit of failure, especially for users who aren't intimately familiar with interop code or the requirements. In the case that a
The latter, as covered by https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd. The premise is that |
In general I'd recommend either taking I'd also lean towards matching what the docs say and not looking at the implementation. Not only can the implementation change over time (and potentially differ on older or future versions of Windows) but not everyone can look at the implementation and so no one else can replicate or rationalize the "why". If there is a case where the docs are out of sync, we should likely push to ensure those get fixed. Likewise we should push to ensure the SAL annotations are up to date and cover the relevant information where possible. |
A string literal couldn't be used where a
We do take |
I corrected my earlier example method to allocate a proper, null-terminated buffer. I'm thinking I'll go ahead and add a null-termination check to the friendly overload as well. |
FYI I went ahead and did this in #555 |
Hi! Pardon me but I think I'm missing something, I'd been under the impression the fix for this issue would include a friendly overload for internal static extern int GetWindowText(winmdroot.Foundation.HWND hWnd, winmdroot.Foundation.PWSTR lpString, int nMaxCount); Is that still coming in another issue or was it deemed impossible? It's hard for me to parse where y'all landed on that from the above discussion. Thanks!! |
@palenshus, this issue was about In your |
Ah I see, makes sense, thanks! I filed #614, though I'm not positive that either the "current way to do it" code or any of the proposals are correct, just trying to get the ball rolling. Thanks! |
Today, accessing such a method requires:
With
StringBuilder
in its friendly overload it reduces to just this:Based on feedback from @riverar.
The text was updated successfully, but these errors were encountered: