본문 바로가기

C#

C#에서 네이티브 코드와 상호작용하는 방법

C#은 관리 코드와 네이티브(비관리) 코드를 손쉽게 연결할 수 있는 여러 가지 메커니즘을 제공합니다. 여기서는 가장 많이 사용되는 P/Invoke와 C++/CLI, 그리고 최신 .NET 7+의 NativeAOT 연동 방법을 간략히 살펴보겠습니다.

1. P/Invoke (Platform Invocation Services)

P/Invoke는 Windows API, C 라이브러리 등 기존 네이티브 DLL 함수를 직접 호출할 때 사용합니다. DllImport 특성을 이용해 함수 시그니처를 선언하면 됩니다.

using System;
using System.Runtime.InteropServices;

class NativeMethods
{
    // winmm.dll의 Beep 함수 호출 예시
    [DllImport("winmm.dll", SetLastError = true)]
    public static extern bool Beep(uint dwFreq, uint dwDuration);

    // 사용자 정의 C 라이브러리 (myutils.dll) 함수
    [DllImport("myutils.dll", CallingConvention = CallingConvention.Cdecl)]
    public static extern int Add(int a, int b);
}

class Program
{
    static void Main()
    {
        // Beep 실행
        NativeMethods.Beep(750, 300);
        // Add 함수 호출
        int result = NativeMethods.Add(5, 7);
        Console.WriteLine($"Add result: {result}");
    }
}

주요 포인트

  • DllImport에 지정하는 문자열은 DLL 파일명(확장자 포함)입니다.
  • 매개변수와 반환형은 정확히 매핑해야 합니다. 잘못 매핑하면 런타임 오류나 메모리 손상이 발생할 수 있습니다.
  • 필요에 따라 CharSet, CallingConvention, SetLastError 등을 설정합니다.

2. C++/CLI를 활용한 래퍼 클래스

P/Invoke가 간단한 함수 호출에 적합하지만, 복잡한 구조체·클래스·콜백을 다루려면 C++/CLI가 유리합니다. C++/CLI 프로젝트에서 네이티브 코드를 래핑하고, 이를 C#에서 관리 코드처럼 사용할 수 있습니다.

// C++/CLI 프로젝트 (MyNativeWrapper)
#pragma once

// 네이티브 헤더 포함
#include "NativeLib.h"

using namespace System;

namespace MyNativeWrapper {
    public ref class Calculator
    {
    private:
        ::NativeCalculator* nativePtr; // 네이티브 객체 포인터
    public:
        Calculator() { nativePtr = new ::NativeCalculator(); }
        ~Calculator() { this->!Calculator(); }
        !Calculator() { delete nativePtr; }

        int Add(int a, int b) { return nativePtr->Add(a, b); }
    };
}

위와 같이 만든 어셈블리를 C# 프로젝트에 참조하면 다음과 같이 사용할 수 있습니다.

using MyNativeWrapper;

class Program
{
    static void Main()
    {
        var calc = new Calculator();
        int sum = calc.Add(10, 15);
        Console.WriteLine($"Sum = {sum}");
    }
}

3. .NET 7+ NativeAOT와 함수 포인터

.NET 7부터는 NativeAOT(전역 네이티브 실행 파일)와 UnmanagedCallersOnly 특성을 이용해 C# 메서드를 네이티브에서 직접 호출할 수 있습니다. 이는 성능이 중요한 게임 엔진이나 임베디드 환경에서 유용합니다.

using System;
using System.Runtime.InteropServices;

class NativeExport
{
    // 네이티브에서 호출 가능한 메서드
    [UnmanagedCallersOnly(EntryPoint = "Multiply")]
    public static int Multiply(int a, int b) => a * b;
}

// 네이티브 C 코드 (call_csharp.c)
/*
extern int Multiply(int a, int b);

int main()
{
    int r = Multiply(6, 7);
    printf("Result: %d\n", r);
    return 0;
}
*/

빌드 옵션 예시 (dotnet CLI)

dotnet publish -c Release -r win-x64 -p:PublishAot=true

생성된 exe는 네이티브 DLL처럼 동작하며, C/C++ 코드에서 직접 함수를 호출할 수 있습니다.

4. 실전 팁

  • 구조체를 전달할 때는 StructLayout(LayoutKind.Sequential)를 명시하고, 필드 정렬을 정확히 맞춥니다.
  • 콜백이 필요하면 delegate를 정의하고 Marshal.GetFunctionPointerForDelegate 로 함수 포인터를 얻어 네이티브에 전달합니다.
  • 디버깅이 어려운 경우 Dependency Walker 혹은 dumpbin /exports 로 DLL 내 함수를 확인하고, SetLastError = trueMarshal.GetLastWin32Error() 로 오류 원인을 파악합니다.

위 내용만 숙지해도 C#과 기존 네이티브 라이브러리 간의 연동을 손쉽게 구현할 수 있습니다. 상황에 맞는 방법을 선택해 효율적인 인터옵을 구현해 보세요.