덧글은 게시글 제목 혹은 본문 아래 덧글 갯수 부분을 클릭하면 작성 할 수 있습니다.
본문 중의 소스 코드를 복사할 경우 더블클릭 후에 복사해야 한줄로 복사되지 않고 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)이 된다는 말은 안해도 알아야 하겠지. 또한 불변 인자를 포함하는 경우의 설명을 달리 요구하지 않아야 예쁜 사람이고...

set 타입의 내부 구조와 엘리먼트 수 알아내기


집합형(set type)은 내부적으로 bit array 라고 할 수 있고,
엘리먼트 갯수가 8개 이하이면 1바이트를 할당해 각 비트를 세팅해 표현하고
9개 이상 16개 이하면 2바이트를 할당, 17개 이상 24개 이하면 3바이트를 할당하는 식으로
32바이트까지, 즉 256개까지의 엘리먼트를 사용할 수 있는 구조이다(FPC에서는 256바이트까지 가능).

선뜻 이해가 안간다면...

TMyEnum = (me1, me2, me3, me4, me5);
    TMySet = set of TMyEnum;

의 경우 TMySet은 엘리먼트가 8개 이하(5개)이므로 SizeOf(TMySet) 하면 1이 되고,

TMySet = set of Char;

의 경우 TMySet은 엘리먼트가 256개이므로 SizeOf(TMySet) 하면 32가 된다는 말이다.
각 비트로 엘리먼트를 표현하므로 256개의 엘리먼트를 표현하기 위해서는 256bit 즉, 32Byte가 필요하기 때문이다.

전자의 경우

MySet: TMySet = [me1, me3];

와 같이 했다면 MySet은 내부적으로 이진수 00000101, 즉 십진수 5의 값을 가진다. 이해했는가?

후자의 경우

MySet: TMySet = ['A'];

와 같이 했다면 MySet은 내부적으로

0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000010 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000

의 값을 가진다. 'A'의 아스키 코드가 65임을 기억한다면 66번째 비트가 1인 것을 알아차릴 것이다.

이것을 알고 있다면 TMySet 형의 변수가 담고있는 엘리먼트 수를 구하는 것은 쉽게 다음 두가지의 처리로 가능하다
1, 크기를 알아낸다.
2. set 된 비트수를 알아낸다.

크기는 SizeOf(MySet) 하면 된다. set된 비트수는


Result := 0;
    for i := 0 to SizeOf(MySet) - 1 do begin
        Temp := Ord(PChar(@MySet)[i]);
        for j := 0 to 7 do begin
            if (Temp and 1) = 1 then Inc(Result);
            Temp := Temp shr 1;
        end;
    end;

와 같이하면 알아낼 수 있을 것이다(당연하지만, i, j, Result는 Integer, Temp는 Byte이다).



set 타입의 구조에 대해서 알아보고 엘리먼트 갯수를 카운팅해보았지만, 프로그래밍에서는 때때로 이것만으로는 부족하다. 위 코드는 특정 set 타입에 대해서는 동작하지만 여러 가지 set 타입에 대해서 엘리먼트 갯수를 구하는 범용 함수로는 사용할 수 없다. 이것이 가능하려면 Ordinal 타입에 대해서도 클래스 타입과 같은 레퍼런스형이 존재해야하는데 이는 지원되지 않는다.

그럼 어떤 방법이 있을까? 그래서 한가지 기법을 더 소개하겠다. 아래를 보자.

function GetElementsCountOfSet(const aTypeInfo: PTypeInfo; const aVar): Integer;
    var
        TypeData: PTypeData;
        Temp: Byte;
        i, j: Integer;
    begin
        TypeData := GetTypeData(GetTypeData(aTypeInfo)^.CompType^);
        Result := 0;
        for i := 0 to TypeData^.MaxValue div 8 do begin
            Temp := Ord(PChar(@aVar)[i]);
            for j := 0 to 7 do begin
                if (Temp and 1) = 1 then Inc(Result);
                Temp := Temp shr 1;
            end;
        end;

위 함수를 다음과 같이 호출하면 MySet의 엘리먼트 갯수를 되돌려준다.

GetElementsCountOfSet(TypeInfo(TMySet), MySet);


P.S. 위 코드 중에 사용된 함수를 모르는 초심자들이 있을 것 같아 노파심에 추가하는데 위 함수는 uses 절에 TypInfo 유닛을 추가해줘야한다. 질문 사항은 덧글로...