C# .NET 8.0에서 LibraryImport 사용하기
닷넷 7.0 이전 버전에서는 대부분 외부 DLL 파일을 Import 하거나 P/Invoke1를 활용하여 Native DLL(Unmanaged DLL)에 있는 함수를 호출하기 위해 DllImport를 사용해 왔다. 8.0버전부터는 LibraryImport의 사용을 권장한다.
LibraryImport가 새로 생겨난 이유는 DllImport가 마샬링을 런타임에서 수행해서 IL 코드를 emit 한다고 하는데 NativeAOT 등 동적으로 IL 코드를 생성할 수 없는 환경에서 쓸 수 없으므로 LibraryImport의 소스 생성기 기능을 이용해 컴파일 시점에서 마샬링 코드를 삽입한다.2
기존에 작성했던 DllImport에서 LibraryImport 스타일로 변경할 때 몇 가지 변경 사항이 있는데 이 부분을 간단한 예제를 통하여 정리하였다. ~.csproj를 열어 AllowUnsafeBlocks를 true로 설정한다.
WPF에서 윈도우 이동(DragMove)
[LibraryImport("user32.DLL", EntryPoint = "ReleaseCapture", StringMarshalling = StringMarshalling.Utf16)]
[UnmanagedCallConv(CallConvs = [typeof(CallConvStdcall)])]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool ReleaseCapture();
[LibraryImport("user32.DLL", EntryPoint = "SendMessageW", StringMarshalling = StringMarshalling.Utf16)]
[UnmanagedCallConv(CallConvs = [typeof(CallConvStdcall)])]
[return: MarshalAs(UnmanagedType.SysInt)]
private static partial IntPtr SendMessageW(IntPtr hWnd, uint wMsg, IntPtr wParam, IntPtr lParam);
[LibraryImport("USER32.DLL", EntryPoint = "FindWindowW", StringMarshalling = StringMarshalling.Utf16)]
[UnmanagedCallConv(CallConvs = [typeof(CallConvStdcall)])]
[return: MarshalAs(UnmanagedType.SysInt)]
private static partial IntPtr FindWindowW(string? lpClassName, string? lpWindowName);
public static void DragMoveWindow(string windowTitle)
{
// MainWindow? mainWindow = Application.Current.MainWindow as MainWindow;
// _ = ReleaseCapture();
// _ = SendMessageW(mainWindow == null ? Process.GetCurrentProcess().MainWindowHandle : FindWindowW(null, mainWindow.Title),
// 0x112, 0xf012, 0);
_ = ReleaseCapture();
_ = SendMessageW(FindWindowW(null, windowTitle), 0x112, 0xf012, 0);
}
INI Read/Write 예제
[LibraryImport("kernel32.dll", EntryPoint = "GetPrivateProfileStringW", StringMarshalling = StringMarshalling.Utf16)]
[UnmanagedCallConv(CallConvs = [typeof(CallConvStdcall)])]
private static partial void GetPrivateProfileStringW(string lpAppName, string lpKeyName, string lpDefault, [Out] char[] lpReturnedString, int nSize, string lpFileName);
[LibraryImport("kernel32.dll", EntryPoint = "WritePrivateProfileStringW", StringMarshalling = StringMarshalling.Utf16)]
[UnmanagedCallConv(CallConvs = [typeof(CallConvStdcall)])]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool WritePrivateProfileStringW(string lpAppName, string lpKeyName, string lpString, string lpFileName);
private const string INI_FILE = "file_path";
public static string Read(string key)
{
try
{
char[] _output = new char[1024];
GetPrivateProfileStringW(SECTION, key, "", _output, output.length, INI_FILE);
output = output.Where(c => c != '\0').ToArray();
return new string(_output);
}
catch (Exception _ex)
{
LogHelper.Logger.Debug($"IniHelper Read ERROR : {_ex.Message}");
return string.Empty;
}
}
public static void Write(string key, string value)
{
try
{
_ = WritePrivateProfileStringW(SECTION, key, value, INI_FILE);
}
catch (Exception _ex)
{
LogHelper.Logger.Debug($"IniHelper Write ERROR : {_ex.Message}");
}
}
SendMessage IPC 1
private const uint WM_SETTEXT = 0x000C;
[LibraryImport("user32.DLL", EntryPoint = "SendMessageW", StringMarshalling = StringMarshalling.Utf16)]
[UnmanagedCallConv(CallConvs = [typeof(CallConvStdcall)])]
[return: MarshalAs(UnmanagedType.SysInt)]
private static partial IntPtr SendMessageW(IntPtr hWnd, uint wMsg, IntPtr wParam, IntPtr lParam);
[LibraryImport("USER32.DLL", EntryPoint = "FindWindowW", StringMarshalling = StringMarshalling.Utf16)]
[UnmanagedCallConv(CallConvs = [typeof(CallConvStdcall)])]
[return: MarshalAs(UnmanagedType.SysInt)]
private static partial IntPtr FindWindowW(string? lpClassName, string? lpWindowName);
public static async Task SendMessage(string message)
{
await Task.Run(() =>
{
try
{
IntPtr handle = FindWindowW(null, "TITLE_XXX");
_ = SendMessageW(handle, WM_SETTEXT, IntPtr.Zero, Marshal.StringToHGlobalAuto(message));
}
catch (Exception ex)
{
LogHelper.Logger.Error($"SendMessage ERROR : {ex.Message}");
}
});
}
// 받는 부분
private static IntPtr WndProc(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
try
{
switch (msg)
{
case 0x0011: // WM_QUERYENDSESSION
_mainWindow.Close();
handled = true;
break;
case 0x000C: // WM_SETTEXT
ReceiveMessage(Marshal.PtrToStringAuto(lParam) ?? string.Empty);
handled = true;
break;
// case 0x0400: // WM_USER
// ReceiveMessageUser(Marshal.PtrToStringAuto(lParam) ?? "");
// handled = true;
// break;
// case 0x004A: // WM_COPYDATA
// ReceiveMessageCopyData(Marshal.PtrToStructure<CopyDataStruct>(lParam).LpData);
// handled = true;
// break;
}
return IntPtr.Zero; // 변경
}
catch (Exception ex)
{
LogHelper.Logger.Error($"WndProc Error : {ex.Message}");
return IntPtr.Zero;
}
}
SendMessage IPC 2 (IPC 1 수정본)
[StructLayout(LayoutKind.Sequential)]
struct COPYDATASTRUCT
{
public IntPtr dwData; // 식별자 (필요 시)
public int cbData; // 데이터 크기 (바이트)
public IntPtr lpData; // 실제 데이터 메모리 주소
}
internal static partial class NativeMethods
{
private const string User32 = "user32.dll";
public const uint WM_COPYDATA = 0x004A;
[LibraryImport(User32, EntryPoint = "SendMessageW")]
[UnmanagedCallConv(CallConvs = [typeof(CallConvStdcall)])]
public static partial IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, ref COPYDATASTRUCT lParam);
[LibraryImport(User32, EntryPoint = "FindWindowW", StringMarshalling = StringMarshalling.Utf16)]
public static partial IntPtr FindWindowW(string? lpClassName, string? lpWindowName);
}
public static async Task SendCopyDataAsync(string message)
{
if (string.IsNullOrEmpty(message)) return;
await Task.Run(() =>
{
IntPtr hData = IntPtr.Zero;
try
{
IntPtr hWnd = NativeMethods.FindWindowW(null, "TITLE_XXX");
if (hWnd == IntPtr.Zero) return;
// 1. 문자열을 유니코드 바이트로 변환
byte[] buffer = Encoding.Unicode.GetBytes(message);
int bufferLength = buffer.Length;
// 2. 비관리 메모리 할당 및 복사
hData = Marshal.AllocHGlobal(bufferLength);
Marshal.Copy(buffer, 0, hData, bufferLength);
// 3. 구조체 설정
COPYDATASTRUCT cds = new()
{
dwData = IntPtr.Zero,
cbData = bufferLength,
lpData = hData
};
// 4. 전송 (ref 키워드 사용)
NativeMethods.SendMessage(hWnd, NativeMethods.WM_COPYDATA, IntPtr.Zero, ref cds);
}
catch (Exception ex)
{
LogHelper.Logger.Error($"SendCopyData Error: {ex.Message}");
}
finally
{
// 5. 할당한 메모리 해제
if (hData != IntPtr.Zero) Marshal.FreeHGlobal(hData);
}
});
}
private static IntPtr WndProc(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
if (msg == NativeMethods.WM_COPYDATA)
{
try
{
// 1. lParam을 구조체로 변환
var cds = Marshal.PtrToStructure<COPYDATASTRUCT>(lParam);
// 2. lpData의 내용을 문자열로 복사
if (cds.lpData != IntPtr.Zero && cds.cbData > 0)
{
// 유니코드(2바이트)이므로 길이를 2로 나눔
string received = Marshal.PtrToStringUni(cds.lpData, cds.cbData / 2);
ReceiveMessage(received);
}
handled = true;
return new IntPtr(1); // 성공 반환
}
catch (Exception ex)
{
LogHelper.Logger.Error($"WndProc COPYDATA Error: {ex.Message}");
}
}
return IntPtr.Zero;
}
AOT 활용
// AOT(Native) : TestAot.dll
[UnmanagedCallersOnly(EntryPoint = "GetTest")]
public static IntPtr GetTest(IntPtr inputString)
{
string? tmpString = Marshal.PtrToStringAuto(inputString);
return Marshal.StringToHGlobalAuto("문자열 : " + tmpString);
}
// Managed code
[LibraryImport("TestAot.dll", EntryPoint = "GetTest", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)]
[UnmanagedCallConv(CallConvs = [typeof(CallConvStdcall)])]
[return: MarshalAs(UnmanagedType.SysInt)]
private static partial IntPtr GetTest([MarshalAs(UnmanagedType.LPWStr)] string inputStr);
private string? GetTestEx(string inputString)
{
try
{
return Marshal.PtrToStringAuto(GetTest(inputString));
}
catch (Exception ex)
{
LogHelper.Logger.Error($"GetTestEx ERROR : {ex.Message}");
return (false, ex.Message);
}
}
AOT 활용 2(utf-8)
[UnmanagedCallersOnly(EntryPoint = "GetTest")]
public static IntPtr GetTest(IntPtr inputString)
{
// 비관리 UTF-8 포인터를 관리되는 string으로 변환
string? tmpString = Marshal.PtrToStringUTF8(inputString);
string result = "문자열 : " + (tmpString ?? string.Empty);
// 관리되는 string을 비관리 UTF-8 메모리(CoTaskMem)로 할당하여 반환
// CoTaskMem, Windows/Linux/macOS 공용
return Marshal.StringToCoTaskMemUTF8(result);
}
[LibraryImport("TestAot.dll", EntryPoint = "GetTest", StringMarshalling = StringMarshalling.Utf8)]
private static partial IntPtr GetTest(string inputStr);
private string? GetTestEx(string inputString)
{
IntPtr pResult = IntPtr.Zero;
try
{
// DLL 호출
pResult = GetTest(inputString);
if (pResult == IntPtr.Zero) return null;
// 반환된 UTF-8 포인터를 string으로 읽기
return Marshal.PtrToStringUTF8(pResult);
}
catch (Exception ex)
{
LogHelper.Logger.Error($"GetTestEx ERROR : {ex.Message}");
return null;
}
finally
{
// StringToCoTaskMemUTF8로 할당된 메모리는 FreeCoTaskMem으로 해제
if (pResult != IntPtr.Zero)
{
Marshal.FreeCoTaskMem(pResult);
}
}
}
Reference