Skip to content

function pointers #347

Closed
Closed

Description

@ufcpp

https://github.com/dotnet/csharplang/blob/main/proposals/csharp-9.0/function-pointers.md

元々 static delegate (静的メソッドならアロケーションなしで間接参照できないか?という話)だったけど、どうも unsafe 必須になったみたい。
「アロケーションなしで assembly unload を安全にトラッキングする手段がないから」とのこと。

unsafe 必須となると、ちょっとしたパフォーマンス改善のために頑張って使うというほどじゃなくなる。
なので、interop シナリオ以外での用途があんまりなく、実例込みで説明した方がよさそう。

ufcpp-live/UfcppLiveAgenda#33 (comment)

Activity

  1. ufcpp commented on Jul 25, 2021

    @ufcpp
    OwnerAuthor
  2. ufcpp commented on Sep 29, 2021

    @ufcpp
    OwnerAuthor
  3. ufcpp commented on Mar 11, 2022

    @ufcpp
    OwnerAuthor
  4. ufcpp commented on May 2, 2022

    @ufcpp
    OwnerAuthor
  5. ufcpp commented on Mar 29, 2023

    @ufcpp
    OwnerAuthor
    using System.Runtime.InteropServices;
    
    var libraryHandle = NativeLibrary.Load("user32.dll");
    var functionPtr = NativeLibrary.GetExport(libraryHandle, "MessageBoxA");
    
    unsafe
    {
        // Define a delegate for the MessageBoxW function
        var messageBox = (delegate* unmanaged[Cdecl]<nint, byte*, byte*, uint, int>)functionPtr;
    
        fixed (byte* p1 = "Hello World!"u8)
        fixed (byte* p2 = "My Caption"u8)
        {
            // Call the MessageBoxW function
            var result = messageBox(0, p1, p2, 1);
    
            Console.WriteLine(result == 1 ? "OK" : "Cancel");
        }
    }
    
    static partial class Lib
    {
        [LibraryImport("user32.dll")]
        public static partial int MessageBoxA(nint hWnd, ReadOnlySpan<byte> lpText, ReadOnlySpan<byte> lpCaption, uint uType);
    }
  6. ufcpp commented on Mar 29, 2023

    @ufcpp
    OwnerAuthor

    win32 API を bind して、primitive な型しか使わない限りは別に DllImport, LibraryImport よりも速いとかもなさげ。

    速度を理由には使わなさそう。
    動的なロードとか、Free したい要件で使うもの?

    using BenchmarkDotNet.Attributes;
    using BenchmarkDotNet.Running;
    using System.Runtime.InteropServices;
    
    BenchmarkRunner.Run<PInvokeBenchmark>();
    
    public class PInvokeBenchmark
    {
        [Benchmark]
        public DateOnly UseNativeLibrary()
        {
            ByNativeLibrary.GetSystemTime(out var t);
            return new(t.Year, t.Month, t.Day);
        }
    
        [Benchmark]
        public unsafe DateOnly UseLibraryImport()
        {
            ByLibraryImport.GetSystemTime(out var t);
            return new(t.Year, t.Month, t.Day);
        }
    
        [Benchmark]
        public unsafe DateOnly UseDllImport()
        {
            ByDllImport.GetSystemTime(out var t);
            return new(t.Year, t.Month, t.Day);
        }
    }
    
    unsafe static class ByNativeLibrary
    {
        private static nint _ptr = NativeLibrary.GetExport(NativeLibrary.Load("kernel32.dll"), "GetSystemTime");
    
        public static void GetSystemTime(out SystemTime systemTime)
        {
            var getSystemTime = (delegate* unmanaged[Cdecl]<out SystemTime, void>)_ptr;
            getSystemTime(out systemTime);
        }
    }
    
    static partial class ByLibraryImport
    {
        [LibraryImport("kernel32.dll")]
        public static partial void GetSystemTime(out SystemTime systemTime);
    }
    
    static partial class ByDllImport
    {
        [DllImport("kernel32.dll")]
        public static extern void GetSystemTime(out SystemTime systemTime);
    }
    
    struct SystemTime
    {
        public ushort Year;
        public ushort Month;
        public ushort DayOfWeek;
        public ushort Day;
        public ushort Hour;
        public ushort Minute;
        public ushort Second;
        public ushort Milliseconds;
    }
  7. ufcpp commented on Mar 29, 2023

    @ufcpp
    OwnerAuthor

    dotnet/runtime 内、ちらほら

    NativeLibrary.TryGetExport(kernel32, "GetTempPath2W", out IntPtr func)
    IntPtr pfnGetSystemTime = NativeLibrary.GetExport(kernel32Lib, "GetSystemTimeAsFileTime")

            if (!NativeLibrary.TryLoad($"{Interop.Libraries.MsQuic}.{s_minMsQuicVersion.Major}", typeof(MsQuicApi).Assembly, DllImportSearchPath.AssemblyDirectory, out IntPtr msQuicHandle) &&
                !NativeLibrary.TryLoad(Interop.Libraries.MsQuic, typeof(MsQuicApi).Assembly, DllImportSearchPath.AssemblyDirectory, out msQuicHandle))
  8. ufcpp commented on Mar 29, 2023

    @ufcpp
    OwnerAuthor

    .NET IL の仕様的には .NET Framework 1.0 の頃から関数ポインターの仕様持ってる。

    関数ポインターのアドレスを IntPtr で取る手段も同じく 1.0 の頃から、
    https://learn.microsoft.com/ja-jp/dotnet/api/system.runtimemethodhandle.getfunctionpointer?view=net-7.0
    で取れた。

    問題はそれを C# から効率よく invoke する手段がなかったこと。
    これも、IL 的には ldftn, ldvirtftn, ldtoken, calli 命令使ってできたけども、C# コンパイラーがこれらの命令を直接生成することはなかった。
    (ILEmit すればできた。)

    「卵が先か」問題になるけども、効率よく invoke できないから関数ポインターの使い道限られてた。

    delegate* 記法を使った関数ポインターが C# に導入されたことで、関数ポインターに対して f(x, y, z) みたいなコードが書けるようになって、これが calli 生成するようになった。
    .NET Core 3.0 で、 NativeLibrary クラスが追加されて、DLL 中の export 関数を関数ポインターで取る手段が入った。
    これで、ネイティブDLLを動的にロードして、その中の関数を calli で呼べるように。

  9. ufcpp commented on Apr 1, 2023

    @ufcpp
    OwnerAuthor
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions