C# - 리눅스 환경에서 DllImport 대신 라이브러리 동적 로드 처리
개인적으로 생각하는 DllImport의 단점이라면 경로를 런타임에 결정할 수 없다는 점입니다. (업데이트 2020-12-16: 이를 위해 NativeLibrary.SetDllImport를 사용할 수 있습니다.)
[DllImport("myDLL.dll")]
public static extern int DLLFunction();
그래도 윈도우 환경이라면 SetDllDirectory Win32 API를 이용하면,
SetDllDirectory
; https://learn.microsoft.com/ko-kr/windows/desktop/api/winbase/nf-winbase-setdlldirectorya
그런대로 경로를 런타임에 결정하는 것과 유사한 효과를 낼 수 있습니다.
그런데 리눅스 환경에 오니 이게 좀 문제가 다릅니다. 같은 경로에 있는 so 파일조차도 기본적으로 로드가 안되므로 동적 로드 방식으로 우회하는 것을 고려해야 합니다.
리눅스 C/C++ - 공유 라이브러리 동적 로딩 후 export 함수 사용 방법
; https://www.sysnet.pe.kr/2/0/11847
이 방식을 사용하기 전, 가능한 /etc/ld.so.conf.d/ 디렉터리에 파일을 생성할 수 있거나 LD_LIBRARY_PATH 환경 변수를 설정할 수 있다면 그렇게 처리하는 것이 더 좋습니다.
위의 글에서 설명한 대로 리눅스에서의 동적 로드는 libdl.so에 있는 3개의 함수를 가져다 쓰면 됩니다. 실습을 위해, 지난 글에서 C++와 .NET 언어 간의 환경 변수 공유가 안 된다고 했는데,
getenv, setenv가 언어/운영체제마다 호환이 안 되는 문제
; https://www.sysnet.pe.kr/2/0/11846
이를 해결하기 위해 C/C++ so 라이브러리 측에 다음과 같이 환경 변수 set/get 함수를 마련하고,
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
extern "C"
{
void SetEnv(char* key, char* value)
{
::setenv(key, value, 1);
}
int GetEnv(char* key, char* pOut, int bufInSize)
{
char *value = ::getenv(key);
if (value == nullptr)
{
pOut = nullptr;
return 0;
}
int len = strnlen(value, bufInSize);
if (len == bufInSize)
{
pOut = nullptr;
return 0;
}
memset(pOut, 0, bufInSize);
strcpy(pOut, value);
return len;
}
}
이 함수를 C# 측에서 이런 식으로 가져다 쓸 수 있습니다.
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace CoreCoreLin
{
public class LibProxy : IDisposable
{
[DllImport("libdl.so", CharSet = CharSet.Ansi)]
static extern IntPtr dlopen(string filePath, RTLD flag);
[DllImport("libdl.so", CharSet = CharSet.Ansi)]
static extern IntPtr dlerror();
[DllImport("libdl.so", CharSet = CharSet.Ansi)]
static extern IntPtr dlsym(IntPtr handle, string symbol);
[DllImport("libdl.so", CharSet = CharSet.Ansi)]
static extern int dlclose(IntPtr pHandle);
[UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
private delegate void SetEnvDelegate(string key, string value);
SetEnvDelegate m_pSetEnvFunc;
[UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
private delegate int GetEnvDelegate(string key, IntPtr pOut, int bufInSize);
GetEnvDelegate m_pGetEnvFunc;
public bool Initialized
{
get
{
return (_pLibHandle != IntPtr.Zero
&& m_pSetEnvFunc != null
&& m_pGetEnvFunc != null);
}
}
public void Dispose()
{
if (_pLibHandle == IntPtr.Zero)
{
return;
}
dlclose(_pLibHandle);
}
IntPtr _pLibHandle;
public LibProxy()
{
string libPath = "/usr/local/lib/libTest.so";
// 이미 동일한 so 파일이 로드된 상태라면 그것을 사용
foreach (ProcessModule mod in Process.GetCurrentProcess().Modules)
{
if (mod.ModuleName.IndexOf("libTest", StringComparison.CurrentCultureIgnoreCase) != -1)
{
libPath = mod.FileName;
Console.WriteLine("libTest FOUND: " + libPath);
break;
}
}
if (string.IsNullOrEmpty(libPath) == true)
{
Console.WriteLine("libTest NOT-FOUND: " + libPath);
return;
}
IntPtr pHandle = dlopen(libPath, RTLD.LAZY);
if (pHandle == IntPtr.Zero)
{
Console.WriteLine("dlopen failed: " + libPath);
return;
}
_pLibHandle = pHandle;
m_pSetEnvFunc = GetProcAddress<SetEnvDelegate>("SetEnv");
m_pGetEnvFunc = GetProcAddress<GetEnvDelegate>("GetEnv");
Console.WriteLine("dlopen succeeded!");
}
T GetProcAddress<T>(string name) where T : class
{
IntPtr pFunc = dlsym(_pLibHandle, name);
if (pFunc == IntPtr.Zero)
{
Console.WriteLine("dlsym - ResolveFailed: " + name);
return null;
}
return Marshal.GetDelegateForFunctionPointer<T>(pFunc);
}
public void SetEnvironmentVariable(string key, string value)
{
if (m_pSetEnvFunc == null)
{
return;
}
m_pSetEnvFunc(key, value);
}
public string GetEnvironmentVariable(string key)
{
if (m_pGetEnvFunc == null)
{
return null;
}
unsafe
{
byte* ptr = stackalloc byte[1024];
IntPtr pBuf = new IntPtr(ptr);
int len = m_pGetEnvFunc(key, pBuf, 1024);
if (len == 0)
{
return null;
}
string txt = Marshal.PtrToStringUTF8(pBuf);
return txt;
}
}
}
[Flags]
public enum RTLD : int
{
LAZY = 0x00001,
NOW = 0x00002,
BINDING_MASK = 0x3,
NOLOAD = 0x00004,
DEEPBIND = 0x00008
}
}
참고로, DllImport에 지정한 so 파일을 찾지 못한 경우 다음과 같은 식의 오류가 발생합니다.
fail: Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware[0]
An unhandled exception has occurred: Unable to load DLL 'libTest.so': The specified module or one of its dependencies could not be found.
(Exception from HRESULT: 0x8007007E)
System.DllNotFoundException: Unable to load DLL 'libTest.so': The specified module or one of its dependencies could not be found.
(Exception from HRESULT: 0x8007007E)
at CoreCoreLin.Controllers.HomeController.internalGetEnv(String key, IntPtr value, Int32 bufSize)
at CoreCoreLin.Controllers.HomeController.SetEnvVar() in D:\TestWebApp\Controllers\HomeController.cs:line 74
at CoreCoreLin.Controllers.HomeController.Index() in D:\TestWebApp\Controllers\HomeController.cs:line 40
at lambda_method(Closure , Object , Object[] )
at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.<InvokeActionMethodAsync>d__12.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
...[생략]...
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.<InvokeAsync>d__15.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at Microsoft.AspNetCore.Builder.RouterMiddleware.<Invoke>d__4.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.<Invoke>d__6.MoveNext()
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]