덧글은 게시글 제목 혹은 본문 아래 덧글 갯수 부분을 클릭하면 작성 할 수 있습니다.
본문 중의 소스 코드를 복사할 경우 더블클릭 후에 복사해야 한줄로 복사되지 않고 LineFeed가 포함됩니다.

Delphi에서 가변인자를 사용하는 함수 만들기

언젠가 모 커뮤니티에서 답변으로 달았던 내용이다.

기본적으로 델파이는 가변인자 선언을 지원하지 않는다(2007까지는 맞는데 추후 버전에서 문법적인 확장이 있었는지는 상위버전을 다뤄보지 않아서 모르겠다. 출시때마다 추가된 사항을 문서로 확인하긴 하는데 pascal 호출규약상 간단히는 안되는 문제라 아마 맞을 것이다). 일상적으로 델파이에서의 가변인자가 필요한 경우는 오픈 어레이를 이용하는게 일반적이다(format 함수 등에서처럼 []로 묶은 배열형 인자). 이 경우 선언은 익히 아는 바와 같이 array of type 혹은 타입 미지정의 경우 array of const를 사용한다. 단, 가변인자를 사용하는 함수를 호출하는 것은 가능한데, c++ 등으로 만들어진 외부 함수를 사용하기 위해 지시자 varargs를 제공하고 있기 때문이다.

이제 가변인자를 사용하는 함수를 정의하는 편법을 얘기할 차례다.


원하는 함수명이 ProcWithVariableParam이라고 할 때 다음과 같이 type 선언을 하나 한 다음 인자 하나짜리 함수를 만든다.

type
    TProcWithVariableParam = procedure(const aParam: Integer); cdecl varargs;

procedure ProcWithVariableParam(const aParam: Integer);
begin
    // 어쩌구 저쩌구...
end;


그런 다음 앞서 선언했던 type의 변수를 하나 만들고 정의해 둔 함수를 대입해 초기화 한다.

var
    StubProcWithVariableParam: TFnWithVariableParam = ProcWithVariableParam;


그리고 코드의 어딘가에서 다음처럼 사용하면 된다.

StubProcWithVariableParam(1, 2, 3);
StubProcWithVariableParam(4, 5, 6, 7, 8, 9);


일단 여기까지에 대한 설명...
varargs는 외부 참조시 가변인자형 함수임을 지정해주는 지시자이고 함수 선언에서는 반드시 external 지시자와 함께 동반되어야 하며 가변인자를 지원하는 c++의 지원에 사용되기 때문에 호출규약을 반드시 cdecl로 선언해주어야 한다.

위 예에서는 함수 선언이 아닌 형 선언이라서 external 지시자가 생략 가능 했고, 그걸 이용하는게 트릭의 실체다. 그런 후 지정된 타입으로 변수를 하나 만들고 초기값으로 만들어진 함수를 대입하므로써 정의된 함수를 마치 c/c++로 만들어진 함수인냥 형태를 가변인자형으로 컴파일러에게 속여 준 것이다.

이러고 끝이면 싱겁겠지.
두번째로 위에서 ProcWithVariableParam에서 넘겨받은 인자를 다루는 부분인데 간단치 않지만 뭐 복잡할 것도 없다. 코드부터 보자.


procedure ProcWithVariableParam(const aParams: Integer); cdecl;
const
    MaxParamCount = 255;
var
    ParamCount: Integer;
    Params: array[0..MaxParamCount - 1] of Integer absolute aParams;
    i: Integer;
begin
    for i := 0 to ParamCount - 1 do
        MessageBox(0, PChar(Format('%d', [Params[i]])), '', 0);
end;

위를 보면 인자수가 가변으로 여러개 일 수 있는데 선언은 1개로만 했으므로 따로 인자를 다룰 방법이 필요한데 이를 위해서 로컬 변수로 원하는 타입의 배열을 선언하고 absolute 키워드로 함수 원형에 선언된 파라미터를 지정해 준다. 그런 다음 인자를 다룰 때는 선언해 준 로컬 배열을 통해 다룬다. MaxParamCount는 최대로 받아 들일 인자의 수를 정의했다.


아직 끝나지 않았어~ 문제는 ParamCount 부분이다.
앞서 "속여"줬다는 표현이 있었는데, 실제로는 c호출 규약의 단일인자 함수인데 호출하는 측에서 가변인자인양 인자수가 달라도 컴파일시 오류가 나지않게 해준 것이고 따라서 델파이는 가변인자에 대한 처리는 어떤 것도 지원하지 않기에 함수 내에서 알아서 해야한다. 그 알아서 하는 부분의 첫 번째 과정을 위에서 설명했고 인자 자체는 배열로 다루도록 조치를 해놨다. 그럴려면 몇개의 인자가 넘어왔는지를 카운팅 해야하는데 다음에 온전히 완성된 하나의 예를 보인다. 역시 코드를 보자.

procedure ProcWithVariableParam(const aParams: Integer); cdecl;
const
    MaxParamCount = 255;
var
    StackPointer: Integer;
    ParamCount: Integer;
    Params: array[0..MaxParamCount - 1] of Integer absolute aParams;
    i: Integer;
begin
    asm
        mov StackPointer, EBP
    end;
    ParamCount := PByte(PInteger(StackPointer + 4)^ + 2)^ div SizeOf(Integer);

    for i := 0 to ParamCount - 1 do
        MessageBox(0, PChar(Format('%d', [Params[i]])), '', 0);
end;

cdecl이 호출한 측에서 스택을 정리한다는 것에 착안해 ebp를 저장하고 ebp가 가르키는 직전 번지를 통해 호출한 코드의 다음 실행 주소를 가져온 후 여기에 2를 더해 스택 정리에 쓰이는 add esp, xxx 의 두번째 오퍼랜드인 xxx의 값을 SizeOf(Integer)로 나누어 호출에 사용된 인자 갯수를 구했다. 인자가 Double이라면 같은 SizeOf(Double)이 된다는 말은 안해도 알아야 하겠지. 또한 불변 인자를 포함하는 경우의 설명을 달리 요구하지 않아야 예쁜 사람이고...

댓글 없음:

댓글 쓰기