3

用 .NET 開發程式庫供 Python 呼叫 - Native AOT 應用

 7 months ago
source link: https://blog.darkthread.net/blog/native-aot-w-python/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

用 .NET 開發程式庫供 Python 呼叫

Python 是當今火紅的程式語言,為 AI/Mechine Learning 領域的奧林匹克指定開發語言,在這些領域,你得說 Python 才能享有一等國民的待遇。

身為 C# 已經寫到得心應手的老人,若在 Python 場子遇到刁鑽需求,但用 C# 可以秒殺或已有現成程式庫,此時我有三種選擇:

  1. 尋找對應的 Python 程式庫或範例
    閃開讓專家來是最上策,但有時看緣分,踏破鐵鞋也未有所得
  2. 用 C# 寫好叫 ChatGPT 轉成 Python
    成功率視狀況,看語法是否單純,若涉及 .NET 獨有 API,轉換得費番手腳。但不依賴額外程式庫讓架構簡潔單純,為次佳解
  3. 用 C# 寫程式庫給 Python 呼叫
    也是個選擇。好處是可直接沿用 C# 成果不必費心移植,編成一顆 .dll (Windows)/.dylib (macOS)/.so (Linux) 給 Python 參考即可

今天這篇就來研究第 3 種做法。

這個技術叫 Native AOT,是 .NET 7 正式加入的功能(更早之前為實驗版,7.0 正式移入 core/runtime,8.0 功能更完整),概念是將 .NET 程式編成原生二進位檔,讓執行檔又小(不用 Runtime)又快(不需 JIT 編譯)。

但 NativeAOT 使用上也有一些限制,像是不支援 Assembly.LoadFile、System.Reflection.Emit、C++/CLI、Built-in COM... 等。參考

補充:MVP Poy 有場 NativeAOT 線上講座,推薦給想深入了解 NativeAOT 來龍去脈的同學。

我打算把 阿拉伯數字與中文數字雙向轉換 .NET 函式包成程式庫給 Python 呼叫,體驗用原生程式庫跨平台跨語言整合。

將 .NET 專案寫成 Native AOT 程式庫供 Unmanaged 世界呼叫的做法我主要參考這篇:Building Native Libraries with NativeAOT

完整專案已上傳 Github,這裡只簡要說明重點:

  1. dotnet new classlib -o chtnumconv-naot 建立專案
  2. 修改 .csproj 加上 <PublishAot>true</PublishAot>
  3. 將 Class1.cs 換成 ChtNumConverter.cs
  4. dotnet publish /p:NativeLib=Shared --use-current-runtime 編譯 (註:Windows 要安裝 VS2022 Desktop Development with C++,參考)
    若為 Windows 平台,在 bin\Release\net8.0\win-x64\public 下會有 chtnumconv-naot.dll 及 .pdb,即為 Native AOT 版本的原生二進位程式庫
    Fig1_638414000165790417.png
    大小僅 1.9MB
    Fig2_638414000168016426.png
  5. NativeLib 參數有 Shared 與 Static 兩種,Static 的話,程式庫檔案會在編譯時合併入,包含在應用程式執行檔裡,此時的輸出格式是 .lib (Windows) / .a (Linux/macOS);Shared 的話,程式庫檔為 .dll (Windows) / .so (Linux) / .dylib (macOS),可供多個應用程式共用,並以動態方式載入。如果要給 Python 用,只能選 Shared。參考

ChtNumConvert.cs 的改寫重點如下:

public class ChtNumConverter
{

    // 解析中文數字       
    [UnmanagedCallersOnly(EntryPoint = "parse_cht_num")] 
    public static long ParseChtNum(IntPtr chtNumStringPtr)
    {
        string chtNumString = Marshal.PtrToStringUTF8(chtNumStringPtr)!;
        var isNegative = false;
        /* 略 */
        num += Parse4Digits(chtNumString);
        return isNegative ? -num : num;
    }
    // 轉換為中文數字
    [UnmanagedCallersOnly(EntryPoint = "to_cht_num")]
    public static IntPtr ToChtNum(long n)
    {
        var negtive = n < 0;
        t = Regex.Replace(t, "^一十", "十");
        /* 略 */
        var result = (negtive ? "負" : string.Empty) + t;
        // TODO 研究傳回 UTF-8 的方法
        return Marshal.StringToHGlobalAnsi(result);
    }
    // 釋放記憶體
    [UnmanagedCallersOnly(EntryPoint = "free_mem")]
    public static void FreeMem(IntPtr ptr) => Marshal.FreeHGlobal(ptr);
}

ParseChtNum() 及 ToChtNum() 加上 [UnmanagedCallersOnly(EntryPoint = "method-name")],string 參數型別或傳回型別改為 IntPtr,用 Marshal.PtrToStringUTF8(chtNumStringPtr) 轉成字串,用 Marshal.StringToHGlobalAnsi(result) 將字串傳成 IntPtr,由於 StringToHGlobalAnsi() 會配置 Unmanaged 記憶體,無法由 GC 機制回收,我看範例程式不太有 人處理這個問題,但我還是加了一個 FreeMem(IntPtr ptr) 讓 Python 呼叫以釋放配置的記憶體。(或是有其他更正確的做法,歡迎指正補充)

Marshal.StringToHGlobalAnsi() 會傳回 ANSI/BIG-5 編碼中文,我沒試出傳回 UTF-8 或 Unicode 編碼的好方法,歡迎知道的朋友補充。

再來看 Python 端,我先研究了 Python 呼叫 C/C++ 程式庫的做法,找到這篇:Python c/c++ 整合

簡單歸納 Python 引用 C/C++ 程式庫常用的方案:Python/C API、ctypes、cffi、pybind11、Cython

  1. Python/C API:維護成本高、相容性差,不推薦直接使用
  2. ctypes:Python 內建,不需 Wrapper,不支援 C++
  3. cffi:依賴額外程式庫,不需 Wrapper,不支援 C++
  4. pybind11:特色是支援 C++,優點對 .NET 無效
  5. Cython:把 Python 程式碼編譯成 C 程式碼,等於不是寫 Python 換了新語言,要換 Toolchain 異動偏大

評估後決定用 ctypes。

import ctypes
chtnumconverter = ctypes.cdll.LoadLibrary("X:/Github/chtnumconv-naot/bin/Release/net8.0/win-x64/publish/chtnumconv-naot.dll")
s = '一千零二十四'
n = chtnumconverter.parse_cht_num(s.encode('utf-8'))
print(n)
chtnumconverter.to_cht_num.restype = ctypes.c_char_p
p = chtnumconverter.to_cht_num(65536) # p = pointer to string
# TODO 找出回傳 UTF-8 編碼的方法
print(p.decode('big5'))
chtnumconverter.free_mem(p); # free memory

我最後拼湊出的 Python 程式如上,似懂非懂,但執行成功了!

Fig3_638414000170500224.png

這是人類的一小步(謎之音:別自我膨脹,頂多是咬冷笱抖一下),卻是我的一大步。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK