前篇文章介紹了輕巧但威力強大的 OpenCC,使用 opencc.exe 可輕鬆完成繁簡轉換。

如果我們要在 .NET 裡寫一個函式招喚 OpenCC 將繁體字串轉成簡體字串該怎麼做?

呼叫外部 .exe 這等小事,自然難不倒 .NET 老鳥,生個 System.Diagnostics.Process,給對 exe 路徑,弄兩個隨機暫存檔放待翻文字與輸出結果,等待 opencc.exe 執行完畢,讀出結果刪掉暫存檔,搞定收工!

    public static class OpenCCConverter
    {

        static string GetPath(string file) => $"X:\Tools\OpenCC\{file}";
        static string GetTempFile() => $"X:\Temp\OpenCCFiles\{Guid.NewGuid()}";

        static void CallOpenCC(string inputFile, string outputFile, string configFile)
        {
            var si = new ProcessStartInfo()
            {
                FileName = GetPath("opencc.exe"),
                Arguments = $"-i {inputFile} -o {outputFile} -c {GetPath(configFile)}",
                CreateNoWindow = true
            };
            var p = new Process()
            {
                StartInfo = si
            };
            p.Start();
            p.WaitForExit();
        }

        /// <summary>
        /// 將繁體轉為簡體
        /// </summary>
        /// <param name="text"></param>
        /// <returns></returns>
        public static string ToChsString(string text)
        {
            var inFile = GetTempFile();
            File.WriteAllText(inFile, text);
            var outFile = GetTempFile();
            CallOpenCC(inFile, outFile, "tw2s.json");
            var result = File.ReadAllText(outFile);
            File.Delete(inFile);
            File.Delete(outFile);
            return result;
        }
    }

這個寫法醜歸醜但很管用,還十分簡單明瞭。只是啟動外部程序成本較高,加上要不斷建檔刪檔,就算只是翻譯一個字元也要動用兩個暫存檔,執行效能及資源使用效率並不好。

無意發現 OpenCC 將核心邏輯放在獨立程式庫 – opencc.dll,何不透過 Interoperability 由 C# 呼叫 C++ 函式直接執行轉換?於是,不知天高地厚的 C++ 麻瓜開啟了 Unmanged DLL 整合大冒險!

先用 Console Application 測試,為求部署方便,我將 OpenCC 納入專案,並設定編譯時輸出到 \bin\opencc 目錄:

開發心得如下:

  1. C# 要呼叫 C++ 寫的 DLL,起手式是用 DllImport 宣告外部函式對應到 C++ 函式,會遇到的挑戰主要是參數的型別傳換。
  2. 在 Github 討論串找到網友 C# DllImport 的程式片段,由於最後有成功,極富參考價值。我學到可先用 opencc_open() 指定轉換設定 json 檔建立 Instance,再呼叫 opencc_convert_utf8() 傳入 Instance Pointer 及待轉換字串,取得結果字串 IntPtr,再轉為 C# 字串。
  3. DllImport 設定不正確時,opencc_open() 時即會出錯,會傳回之類的訊息
    Unable to load DLL 'opencc.dll': The specified module could not be found. (Exception from HRESULT: 0x8007007E)
    我遇過兩種情況:1) DllImport 指定的 opencc.dll 路徑有誤 2) 執行主機缺少 Visual C++ Runtime。
  4. 官方下載的 OpenCC 1.0.1 Windows 版使用 Visual Studio 2012 編譯,需要「Visual Studio 2012 最新支援的 Visual C++ 可轉散發套件」,微軟支援網站有個 最新支援的 Visual C++ 下載 網頁已整理好所有 VC++ 版本的可轉散發套件,請自行依所需版本下載安裝。
    若懷疑跟 C++ Runtime 套件沒裝有關,最簡單的驗證方法是手動執行 opencc.exe,若彈出缺少 msvcp***.dll 之類的錯誤訊息就是了。
  5. opencc_convert_utf8() 轉換失敗時不會出錯,會傳回 IntPtr.Zero,詳細錯誤訊息需另外呼叫 opencc_error() 取得。
  6. 我一度卡在一個關鍵點,待轉換字串與結果字串,形式為記憶體指標指向一段 UTF8 編碼格式的 byte[],與 string 之間需要特殊函式轉換,我在 Stackoverflow 找到可用範例

瞎弄一陣,萬萬沒想到還真被 C++ 麻瓜試出來了,可執行程式範例如下:

【2024-05-19 更新】程式存在 MemoryLeak 問題,建議改用 CI-YU 版本,感謝 bill 分享。

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            Debug.WriteLine(
                OpenCCHelper.ConvertToChs(
                    "預設記憶體大小與硬碟容量"));
            Console.ReadLine();
        }
    }

    public static class OpenCCHelper
    {
        [DllImport("opencc\\opencc.dll", EntryPoint = "opencc_open")]
        static extern IntPtr opencc_open(string configFileName);

        [DllImport("opencc\\opencc.dll", EntryPoint = "opencc_convert_utf8")]
        static extern IntPtr opencc_convert_utf8(Int64 opencc, IntPtr input, long length);

        static IntPtr OpenCCInstance = IntPtr.Zero;

        static OpenCCHelper()
        {
            OpenCCInstance = opencc_open(".\\opencc\\tw2sp.json");
        }

        //https://stackoverflow.com/a/10773988/288936
//提醒:以下寫法存在 Memory Leak 問題,建議改用 CI-YU 版 public static IntPtr NativeUtf8FromString(string managedString) { int len = Encoding.UTF8.GetByteCount(managedString); byte[] buffer = new byte[len + 1]; Encoding.UTF8.GetBytes(managedString, 0, managedString.Length, buffer, 0); IntPtr nativeUtf8 = Marshal.AllocHGlobal(buffer.Length); Marshal.Copy(buffer, 0, nativeUtf8, buffer.Length); return nativeUtf8; } public static string StringFromNativeUtf8(IntPtr nativeUtf8) { int len = 0; while (Marshal.ReadByte(nativeUtf8, len) != 0) ++len; byte[] buffer = new byte[len]; Marshal.Copy(nativeUtf8, buffer, 0, buffer.Length); Marshal.FreeHGlobal(nativeUtf8 ); return Encoding.UTF8.GetString(buffer); } public static string ConvertToChs(string text) { IntPtr inStr = NativeUtf8FromString(text); IntPtr outStr = opencc_convert_utf8(OpenCCInstance.ToInt64(), inStr, -1); Marshal.FreeHGlobal(inStr); return StringFromNativeUtf8(outStr); } } }

如下圖,我成功呼叫 opencc.dll 完成繁簡轉換。

核子試爆成功是第一步,要寫成共用元件還會再遇到一些問題,例如:x86/x64 必須使用不同 opencc.dll、部署到 ASP.NET 網站時 DllImport 路徑需動態指向網站資料夾、Thread-Safe 考量、Memory Leak 疑慮... C++ 麻瓜大冒險尚未結束,下集待續。

(聲明:程式為門外漢參考爬文及測試所得,如有 C/C++ 高人路過,請鞭小力一點並不吝指正)

Sample code of calling external OpenCC process or unmanaged OpenCC library in C#.


Comments

# by 小歆

可以把你编译好的opencc.dll文件拿来分享一下吗???

# by Radium

StringFromNativeUtf8 里是否需要call opencc_convert_utf8_free 把 nativeUtf8 所指向的内存释放掉? opencc_convert_utf8 函数的声明[1]说: You MUST call opencc_convert_utf8_free() to release allocated memory. [1] https://github.com/BYVoid/OpenCC/blob/master/src/opencc.h

# by Jeffrey

to Radium, 的確,應該要加上 Marshal.FreeHGlobal(nativeUtf8 ); 才對,謝謝你的提醒。

# by bill

好像只有C#有釋放記憶體,opencc的dll還是需要做到釋放才不會有問題?

# by bill

等待複核中,留言將在稍後顯示 / The comment is awaiting review.

# by Jeffrey

to bill, 謝謝研究與分享 ,已補充於文章。

Post a comment