Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 5개 있습니다.)
(시리즈 글이 16개 있습니다.)
.NET Framework: 112. How to Interop DISPPARAMS
; https://www.sysnet.pe.kr/2/0/617

.NET Framework: 137. C#에서 Union 구조체 다루기
; https://www.sysnet.pe.kr/2/0/728

.NET Framework: 141. Win32 Interop - 크기가 정해지지 않은 배열을 C++에서 C#으로 전달하는 경우
; https://www.sysnet.pe.kr/2/0/737

.NET Framework: 168. [in,out] 배열을 C#에서 C/C++로 넘기는 방법
; https://www.sysnet.pe.kr/2/0/810

.NET Framework: 169. [in, out] 배열을 C#에서 C/C++로 넘기는 방법 - 두 번째 이야기
; https://www.sysnet.pe.kr/2/0/811

.NET Framework: 183. 구조체 포인터 인자에 대한 P/Invoke 정의
; https://www.sysnet.pe.kr/2/0/912

.NET Framework: 472. C/C++과 C# 사이의 메모리 할당/해제 방법
; https://www.sysnet.pe.kr/2/0/1784

.NET Framework: 620. C#에서 C/C++ 함수로 콜백 함수를 전달하는 예제 코드
; https://www.sysnet.pe.kr/2/0/11099

.NET Framework: 627. C++로 만든 DLL을 C#에서 사용하기
; https://www.sysnet.pe.kr/2/0/11111

.NET Framework: 686. C# - string 배열을 담은 구조체를 직렬화하는 방법
; https://www.sysnet.pe.kr/2/0/11319

.NET Framework: 757. 포인터 형 매개 변수를 갖는 C++ DLL의 함수를 C#에서 호출하는 방법
; https://www.sysnet.pe.kr/2/0/11533

.NET Framework: 978. C# - GUID 타입 전용의 UnmanagedType.LPStruct
; https://www.sysnet.pe.kr/2/0/12444

C/C++: 158. Visual C++ - IDL 구문 중 "unsigned long"을 인식하지 못하는 #import
; https://www.sysnet.pe.kr/2/0/13128

.NET Framework: 2058. [in,out] 배열을 C#에서 C/C++로 넘기는 방법 - 세 번째 이야기
; https://www.sysnet.pe.kr/2/0/13141

.NET Framework: 2083. C# - C++과의 연동을 위한 구조체의 fixed 배열 필드 사용 (2)
; https://www.sysnet.pe.kr/2/0/13205

닷넷: 2152. Win32 Interop - C/C++ DLL로부터 이중 포인터 버퍼를 C#으로 받는 예제
; https://www.sysnet.pe.kr/2/0/13429





Win32 Interop - 크기가 정해지지 않은 배열을 C++에서 C#으로 전달하는 경우


이 글은 다음의 질문에 대한 답변입니다.

dll import하기 위해 struct 구성시에 struct가 struct를 가지고 있고 포함된 struct가 ByValArray형태일때 해결 
; https://www.sysnet.pe.kr/3/0/808




크기가 정해진 경우에는 구조체만 잘 맞춰주면 COM 메서드나 export된 Win32 API와 연동하는 것은 그다지 어렵지 않습니다.

예전에 설명해드린 아래의 글은 union 구조체인 경우까지도 정상적으로 연동하는 것을 보여주는 예인데요.

C#에서 Union 구조체 다루기
; https://www.sysnet.pe.kr/2/0/728

그런데, 크기가 정해지지 않은 배열의 경우에는 상황이 다릅니다. COM의 IDL조차도 이런 경우 length_is / size_is로 명시적인 마샬링 방법을 지정해야 하고, 이러한 메서드를 호출할 때 managed 환경에서는 MarshalAs의 SizeParamIndex를 지정해서 연동해야 합니다. 그나마 이렇게 대상이 COM 개체의 메서드라면 IDL 상에서 적당히 attribute만 지정되어 있으면 C#과의 연동이 허용된다는 것이지요.

문제는, 그렇게 정해지지 않은 타입이 구조체의 멤버로 포함되어 있고 이런 구조체를 C/C++에서 C#쪽으로 넘기는 경우에는 매끄럽게 연동하는 방법이 없습니다. 다음과 같은 경우가 그 예일 텐데요.

public struct fng_curve
{
	[MarshalAs(UnmanagedType.ByValArray, SizeConst = 50)]
	public double[] tenor;
};

public struct fng_curve_list
{
	public int size;
	public fng_curve* data;
};

사실, COM을 제대로 아는 사람이라면 위의 구조체를 서비스하는 COM 개체는 잘못 작성되어진 것임을 알 수 있습니다. 무슨 이야기냐하면, COM조차도 fng_curve_list의 구조체 멤버인 fng_curve* data 내용을 마샬링할 수 없습니다. 이것이 가능한 경우는, 위의 COM 개체를 In-Process로만 사용하는 C/C++ 클라이언트 뿐입니다. 만약 Out-of-Process로 위의 COM 개체를 사용하면 Marshaller는 fng_curve* data 내용을 정상적으로 마샬링하지 못합니다. (사용자 정의 마샬러를 작성한다면 예외겠지만.) 간단하게는 Apartment만 바꾸어도 AV( Access Violation) 예외가 발생하지요.

C/C++ 프로젝트에 /clr 옵션 적용으로 인한 COM 개체 사용 오류
; https://www.sysnet.pe.kr/2/0/650

그러니, 애당초 이런 COM 개체를 만들어서는 안됩니다.




비록 그렇다고는 하지만, C# 클라이언트 역시 같은 EXE 프로세스 공간이라면 이런 경우에 대한 해결책이 있긴 합니다. 바로 이런 경우, fng_curve 포인터 변수 대신에 IntPtr을 써주면 되는 것입니다.

이렇게 다루려면 다음과 같이 정의해 줘야 합니다.

[StructLayout(LayoutKind.Sequential)]
public class fng_curve
{
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 50)]
    public double[] tenor;
}

[StructLayout(LayoutKind.Sequential)]
public struct fng_curve_list
{
    public int size;
    public IntPtr data;
}

구조체 포인터 대신에 IntPtr로 바뀌고, fng_curve 구조체가 클래스로 바뀐 것 빼고는 별다르게 특별한 점은 없습니다.

호출되는 C/C++ 측의 코드와 호출하는 C# 측은 각각 다음과 같습니다.

===== export되는 Win32 API =====
INTEROPWIN32_API void fnInterop1(fng_curve_list *item)
{
	item->size = 5; // 배열 수를 지정하고,
	item->data = new fng_curve[item->size]; // 배열 할당 후 값을 설정
	
	for (int i = 0; i < item->size; i ++)
	{
		for (int j = 0; j < 50; j ++ )
		{
			item->data[i].tenor[j] = j * i;
		}
	}
}

===== Win32 API를 호출하는 C#코드 =====

fng_curve_list item = new fng_curve_list();
fnInterop1(ref item); // 구조체를 ref로 전달하고,

// 반환받은 구조체에서 IntPtr 데이터를 역직렬화
for (int i = 0; i < item.size; i++)
{
    fng_curve curve = new fng_curve();

    IntPtr pAddr = new IntPtr(item.data.ToInt64() + (i * Marshal.SizeOf(curve)));
    Marshal.PtrToStructure(pAddr, curve);
}

Marshal.PtrToStructure 메서드는 이름과는 달리 두 번째 인자에 값 형식의 struct를 전달해주면 안되고 참조 형식의 class를 전달해줘야 합니다. 이 때문에 fng_curve를 기존에 struct로 정의된 것을 class로 재정의한 것입니다.

일단, 이렇게 하면 급한데로 연동은 시켰습니다.




물론, 문제는 여기서 끝나지 않습니다. 제가 말씀드린 데로 위의 COM 개체는 COM에 대한 이해가 부족한 상태에서 제작된 것이기 때문에 치명적인 문제가 있습니다. 너무도 유명한 "Memory Leak"이 발생한다는 것!

C/C++에서 메모리를 "new" 연산자를 통해서 할당한 것은 C/C++의 고유한 메모리 할당방식일 뿐 언어간에 표준으로 자리잡은 것이 아니기 때문에 managed 환경에서의 C#에서 IntPtr로 받은 메모리를 해제할 수 있는 방법이 없습니다. Marshal 타입에서 제공되는 해제 함수들은 AllocHGlobal / CoTaskMem으로 할당되었거나 COM 개체의 참조카운트를 감소시키는 정도일 뿐 C/C++의 new/delete에 상응되는 것은 없습니다. 따라서 이런 경우에는 메모리 해제를 하도록 C/C++ 측에서 메서드를 만들어주고 C#에서는 그것을 반드시 호출해 주어야 합니다.

===== export되는 Win32 API =====
INTEROPWIN32_API void fnInterop2(fng_curve_list *item)
{
	delete [] item->data;
}

===== Win32 API를 호출하는 C#코드 =====
fng_curve_list item = new fng_curve_list();
fnInterop1(ref item); // 사용 후,

fnInterop2(ref item); // 반드시 메모리 해제




일단, 위에서는 접근방식을 가능한 managed 환경으로 처리를 해보았는데 unsafe 키워드를 적절하게 사용하면 좀 더 직관적인 구문으로 처리하는 것도 됩니다. 2가지 방식으로 처리한 예제 코드를 다음에 첨부해 놓았습니다.

- managed 버전으로 처리: InteropTest(managed).zip
- unsafe 버전으로 처리: InteropTest(unsafe).zip



[이 토픽에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 10/27/2023]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




... 76  77  78  79  80  81  [82]  83  84  85  86  87  88  89  90  ...
NoWriterDateCnt.TitleFile(s)
11760정성태10/24/201813388개발 환경 구성: 418. Azure - Runbook 내에서 또 다른 Runbook 스크립트를 실행
11759정성태10/24/201814262개발 환경 구성: 417. Azure - Runbook에서 사용할 수 있는 다양한 메서드를 위한 부가 Module 추가
11758정성태10/23/201816337.NET Framework: 800. C# - Azure REST API 사용을 위한 인증 획득 [3]파일 다운로드1
11757정성태10/19/201813419개발 환경 구성: 416. Visual Studio 2017을 이용한 아두이노 프로그램 개발(및 디버깅)
11756정성태10/19/201816172오류 유형: 500. Visual Studio Code의 아두이노 프로그램 개발 시 인텔리센스가 안 된다면?
11755정성태10/19/201817495오류 유형: 499. Visual Studio Code extension for Arduino - #include errors detected. [1]
11754정성태10/19/201814201개발 환경 구성: 415. Visual Studio Code를 이용한 아두이노 프로그램 개발 - 새 프로젝트
11753정성태10/19/201822078개발 환경 구성: 414. Visual Studio Code를 이용한 아두이노 프로그램 개발
11752정성태10/18/201814408오류 유형: 498. SQL 서버 - Database source is not a supported version of SQL Server
11751정성태10/18/201814488오류 유형: 497. Visual Studio 실행 시 그래픽이 투명해진다거나, 깨진다면?
11750정성태10/18/201812940오류 유형: 496. 비주얼 스튜디오 - One or more projects in the solution were not loaded correctly.
11749정성태10/18/201814661개발 환경 구성: 413. 비주얼 스튜디오에서 작성한 프로그램을 빌드하는 가장 쉬운 방법
11748정성태10/18/201815549개발 환경 구성: 412. Arduino IDE를 Store App으로 설치한 경우 컴파일만 되고 배포가 안 되는 문제
11747정성태10/17/201815847.NET Framework: 799. C# - DLL에도 EXE처럼 Main 메서드를 넣어 실행할 수 있도록 만드는 방법파일 다운로드1
11746정성태10/15/201815108개발 환경 구성: 411. Bitvise SSH Client의 인증서 모드에서 자동 로그인 방법파일 다운로드1
11745정성태10/15/201813507오류 유형: 495. TFS 파일/폴더 삭제 - The item [...] could not be found in your workspace, or you do not have permission to access it.
11744정성태10/15/201813797개발 환경 구성: 410. msbuild로 .pubxml 설정에 따른 배포 파일을 만드는 방법
11743정성태10/15/201814741웹: 37. Bootstrap의 dl/dt/dd 조합에서 문자열이 잘리지 않도록 CSS 설정
11742정성태10/15/201821066스크립트: 13. 윈도우 배치(Batch) 스크립트에서 날짜/시간 문자열을 구하는 방법
11741정성태10/15/201814928Phone: 13. Android - LinearLayout 간략 설명
11740정성태10/15/201817139사물인터넷: 51. Synology NAS(DS216+II)를 이용한 원격 컴퓨터의 전원 스위치 제어
11739정성태10/15/201818663Windows: 151. 윈도우 10의 전원 관리가 "균형 조정(Balanced)"으로 바뀌는 문제
11738정성태10/15/201817087오류 유형: 494. docker - 윈도우에서 실행 시 "unknown shorthand flag" 오류 [1]
11737정성태10/13/201812789오류 유형: 493. Azure Kudu - There are ... items in this directory, but maxViewItems is set to 299
11736정성태10/12/201814100오류 유형: 492. Visual Studio 로딩 시 오류 - The 'Scc Display Information' package did not load correctly.
11735정성태10/12/201819744VS.NET IDE: 129. Visual Studio - 특정 문자(열)를 개행 문자로 바꾸는 방법
... 76  77  78  79  80  81  [82]  83  84  85  86  87  88  89  90  ...