LibraryImport: The type '*' is not supported by source-generated P/Invokes

265 Views Asked by At

I am porting this implementation (starting a process as desktop user) from [DllImport] to [LibraryImport] (.NET 8). Visual Studio Roslyn has changed the method 'AdjustTokenPrivileges' to

[LibraryImport("advapi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool AdjustTokenPrivileges(IntPtr htok, [MarshalAs(UnmanagedType.Bool)] bool disall, ref TOKEN_PRIVILEGES newst, int len, IntPtr prev, IntPtr relen);

and I get this error: SYSLIB1051: The type 'MyMethods.TOKEN_PRIVILEGES' is not supported by source-generated P/Invokes. The generated source will not handle marshalling of parameter 'newst'.

And the next method 'CreateProcessWithTokenW' is changed to

[LibraryImport("advapi32", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool CreateProcessWithTokenW(IntPtr hToken, int dwLogonFlags, string lpApplicationName, string lpCommandLine, int dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, [In] ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation);

where the error is: SYSLIB1051: The type 'MyMethods.STARTUPINFO' is not supported by source-generated P/Invokes. The generated source will not handle marshalling of parameter 'lpStartupInfo'.

I tried to change Int32 to int and UInt32 to uint and changed string to unsafe char* as proposed here. I also tried to implement the answer from CaiB but I couldn't get through it.

1

There are 1 best solutions below

0
Simon Mourier On

Well, I don't know what crazy mind has imagined the new soure generation for p/invoke, but it's far from being simple. It's much more powerful than the previous one and let you finely tune things, but there are obvious and simple things that it cannot do alone, and it's generally not 100% compatible, work has to be done. You can add to that the far-from-being-complete documentation.

Ok, so there's the concept of blittable types (a type that is the same at binary level, whatever piece of code, managed or unmanaged, accesses it). It's not new at all but it seems the list of what's blittable has been restricted. Apparently, an array of blittable types, of constant size is not considered as blittable itself anymore...

So, when a type is not blittable, you must marshal the type by yourself, as stated here: https://github.com/dotnet/runtime/issues/75376

This is a known deficiency in the new system. We do not have a source-generator today for generating marshalling information for a user-defined type. You'll need to manually define a marshaller using the new custom type marshaller model if you want to use a non-blittable user-defined type with LibraryImport.

Enough ranting, here is a sample for a marshaller for TOKEN_PRIVILEGES:

[CustomMarshaller(typeof(TOKEN_PRIVILEGES), MarshalMode.ManagedToUnmanagedIn, typeof(TOKEN_PRIVILEGESMarshaller))]
private unsafe static class TOKEN_PRIVILEGESMarshaller
{
    public ref struct Unmanaged
    {
        public uint PrivilegeCount;
        public LUID_AND_ATTRIBUTES* Privileges;
    }

    // note: you can put a breakpoint here and walk the stack
    // back, you will see the inner workings of the generated source
    public static Unmanaged ConvertToUnmanaged(TOKEN_PRIVILEGES managed)
    {
        var unmanaged = new Unmanaged
        {
            PrivilegeCount = managed.PrivilegeCount,
            Privileges = ArrayMarshaller<LUID_AND_ATTRIBUTES, LUID_AND_ATTRIBUTES>.AllocateContainerForUnmanagedElements(managed.Privileges, out var count)
        };
        ArrayMarshaller<LUID_AND_ATTRIBUTES, LUID_AND_ATTRIBUTES>.GetManagedValuesSource(managed.Privileges)
            .CopyTo(ArrayMarshaller<LUID_AND_ATTRIBUTES, LUID_AND_ATTRIBUTES>
            .GetUnmanagedValuesDestination(unmanaged.Privileges, count));
        return unmanaged;
    }

    public static void Free(Unmanaged unmanaged) => ArrayMarshaller<LUID_AND_ATTRIBUTES, LUID_AND_ATTRIBUTES>.Free(unmanaged.Privileges);
}

[NativeMarshalling(typeof(TOKEN_PRIVILEGESMarshaller))]
private struct TOKEN_PRIVILEGES
{
    public uint PrivilegeCount;
    //[MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)] // you can keep that for compat reasons if you share the source with older .NET projects
    public LUID_AND_ATTRIBUTES[] Privileges;
}

Some useful documentation: User Defined Type Marshalling for Source-Generated Interop and Tutorial: Use custom marshallers in source-generated P/Invokes

And you can call it like that:

[LibraryImport("advapi32", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool AdjustTokenPrivileges(IntPtr htok, [MarshalAs(UnmanagedType.Bool)] bool disall, TOKEN_PRIVILEGES newst, int len, IntPtr prev, IntPtr relen);

For the second one, just remove [In], for some reasons, the generator chokes on it:

[LibraryImport("advapi32", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool CreateProcessWithTokenW(IntPtr hToken, int dwLogonFlags, string lpApplicationName, string lpCommandLine, int dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation);

I've put a full source copy here: https://gist.github.com/smourier/70cc9208b47534ba475279a4554180ce

Now, it's interesting to look at the generated code:

enter image description here

As you can see, in the end, it's just calling the method using the DllImport way with the unmanaged struct. So in this case, I don't think it's useful to use LibraryImport at all.

PS: in this very special case, since it's an array of 1 element, you could just declare this:

    private struct TOKEN_PRIVILEGES
    {
        public uint PrivilegeCount;
        public LUID_AND_ATTRIBUTES Privileges;
    }