LazarusCompleteGuide:4.3
DDL과 공유 객체
모든 프로그램에는 메인 루틴이 있다. C 언어에서는 이를 main() 이라 부르는데, 이는 명령 행 파라미터의 수와 내용을 파라미터로 취한다. 일반적으로 C 언어의 main() 함수는 int 타입을 가지는데, 리턴값을 공급함을 의미한다. main() 함수의 리턴값은 프로그램의 리턴값이 될 것이다. 보통 리턴값이 0인 경우 프로그램이 성공적으로 완료되었음을 암시한다. 오브젝트 파스칼 프로그램은 main()라는 이름으로 된 루틴을 명시적으로 가지고 있지는 않지만 내부적으로 그와 같은 과정의 루틴을 가진다. 이것은 프로젝트 파일 내 (라자루스에선 lpr 확장자) 프로그램의 메인 본체이다.
begin
Application.Initialize;
Application.CreateForm(TForm1, Form1);
Application.Run;
end.
위의 시퀀스는 C에서 main() 함수에 해당한다. 애플리케이션의 리턴값은 0으로 자동 사전정의되지만 Halt(1)를 이용해 변경할 수 있다.
TApplication 객체는 애플리케이션이 아니다-라자루스에서 TApplication 은 델파이의 것과 다르다. 이와 관련된 내용은 조금 있다가 다루겠다. 라자루스 프로그램은 항상 프로그램 파일 내 ProgramName; 프로그램으로 시작된다. 메인 프로그램의 정의는 필수다. LibraryName 라이브러리부터 시작해 라이브러리들은 다중 프로그래밍 언어를 수반하는 프로그래밍과 재사용 가능한 소스 코드의 기반이 될 수 있다. 라이브러리에는 두 가지 타입이 있다: dynamic 과 static. 정적 라이브러리는 애플리케이션이 컴파일 될 때 그로 연결되는 반면 동적 라이브러리는 필요 시 런타임에서 로딩되거나 애플리케이션에 정적으로 연결된다. 하지만 라이브러리들은 .so (Linux/Unixes), .dll (Windows), 또는 .dylib (Mac OS X Unix Library) 항상 확장자로 된 메인 실행 파일과 구분된 파일에 위치한다.
동적 라이브러리
동적 라이브러리를 생성할 때는 메인 프로젝트 파일이 program 대신 library 키워드를 포함해야 한다. 또한 내보낼 모든 함수와 프로시저 이름은 메인 프로젝트 파일의 내보내기(export)로 열거되어야 한다 (콤마로 구분).
라이브러리는 메인 프로그램을 갖고 있지 않지만 initialization 코드를 포함할 수 있다. 내부 라이브러리가 아닌 한 (라자루스 자체에서만 사용할 용도) 라이브러리로부터 내보내거나 가져올 모든 함수의 경우에 대해 호출 규칙(calling convention)을 명시해야 한다.
그러한 경우 가져오기와 내보내기에 사전 정의된 규칙이 적용된다. 사전 정의된 규칙은 레지스터에 여유가 있는 한 CPU 레지스터 내 파라미터의 전달을 포함한다. 일부 CPU 레지스터는 특정 용도로 예약되어 있으므로 모든 CPU 레지스터를 사용할 수 있는 것은 아니다. 이는 사용 중인 프로세서에 따라 좌우된다. 함수 리턴값은 누산기(accumulator)에 들어 맞다면 누산기에서 (첫 번째 프로세서 레지스터) 전달된다. 메소드 호출은 (오브젝트 또는 클래스의) 보이지 않는 (invisible) 파라미터, Self 를 가진다. 이 파라미터는 메소드로의 호출 맨 왼쪽에 위치한다 (따라서 메소드로 마지막에 전달된다). 프로시저 또는 함수가 끝이 나면 스택을 제거할 것이다(clean up).
변경자 | 파라미터 순서 | ...에 의해 스택이 제거 | 정렬 | 저장된 레지스터 |
register | 좌측 ⇒ 우측 | 함수 | 기본설정(default) | 없음 |
cdecl | 우측 ⇒ 좌측 | 호출자(caller) | GCC-정렬 | GCC-레지스터 |
interrupt | 우측 ⇒ 좌측 | 함수 | 기본설정 | 모든 레지스터 |
pascal | 좌측 ⇒ 우측 | 함수 | 기본설정 | 없음 |
safecall | 우측 ⇒ 좌측 | 함수 | 기본설정 | GCC-레지스터 |
stdcall | 우측 ⇒ 좌측 | 함수 | GCC-정렬 | GCC-레지스터 |
oldfpccall | 우측 ⇒ 좌측 | 호출자(caller) | 기본설정 | 없음 |
mwpascal | 우측 ⇒ 좌측 | 호출자(caller) | GCC-정렬 | GCC-레지스터 |
표 4.4: x86 프로세서에서 프리 파스칼 호출 규칙과 효과 (라자루스에도 적용) |
외부 오브젝트 파일과 라이브러리를 연결할 때 이용할 수 있는 다른 호출 규칙들도 있는데, 이는 표 4.4에서 소개한다. 첫 번째 열은 프로시저를 선언 시 명시할 수 있는 변경자를 열거한다. 두 번째 열은 파라미터를 스택상에 위치시키는 순서를 제공한다. 세 번째 열은 호출자나 함수 중 어떤 것이 스택 제거를 책임지는지 명시한다. 정렬과 관련된 네 번째 열은 스택 공간에서 파라미터의 정렬에 관한 정보를 보여준다. 다섯 째 열은 서브루틴의 initial 코드에 저장되는 레지스터를 (해당할 경우) 알려준다.
이용 가능한 호출 규칙에 관한 추가 정보는 Programmer's Manual for Free Pascal (저자: Michael Van Canneyt: Free Pascal 2, 출판사: C&L Verlag, ISBN 978-3-936546-53-8)에 발표된다. 내보내기와 가져오기에도 동일한 규칙이 적용되어야 함을 명심하는 것이 중요하다. 아래 목록은 SwitchNumber라는 프로시저를 내보내는 동적 라이브러리를 보여준다:
library numbers;
{$mode delphi}
procedure SwitchNumbers(var A, B: Integer); / exported procedure
var C: Integer;
begin
C := B;
B := A;
A := C;
end;
exports // export definition
SwitchNumbers; // Case-sensitive!
end.
어떤 구체적 호출 규칙도 나타나지 않았으므로 해당 라이브러리는 프리 파스칼 프로그램 내부에서만 사용할 수 있다. 라이브러리는 많은 수의 프로시저와 함수를 내보낼 수 있으며, 내보내진 루틴은 굳이 프로젝트 파일에서 정의되지 않아도 된다. 호환성을 위해 객체 지향 루틴은 (예: 메소드) 내보내지 않길 권하는데, 만일 이를 어길 시 다른 프로그래밍 언어로 가져오기(import)가 복잡해질 수 있기 때문이다. 객체 지향 루틴을 가져올 경우, 앞서 언급한 Self 파라미터를 준비해야 한다. 그 외에도 exports 블록에 명시된 바와 정확히 동일하게 루틴을 가져오도록 확실히 해야 한다. 내보내기와 가져오기는 대・소문자를 구별한다:
callnumbers 프로그램은 아래 라이브러리를 호출한다:
program callnumbers;
{$apptype console}
{$mode delphi}
// static import of the procedure from the library, loaded into memory during
// the initialization of the program
procedure SwitchNumbers(var A, B: integer); external 'numbers';
// the main program
var A, B: integer;
begin
A := 5;
B := 10;
WriteLn('Original data: A:',A, ' B: ',B);
SwitchNumbers(A, B);
Writeln('Values after the call to SwitchNumbers: A:',A, ' B:',B);
end.
이 예에서 동적 라이브러리는ㅡ호출된 루틴이 아니라ㅡ프로그램에 정적으로 연결되었다. 그 결과, 라이브러리가 누락되면 프로그램을 시작할 수 없고 오류 메시지와 함께 중단될 것이다. 우리는 라이브러리가 실제로 필요할 때만 로딩시킴으로써 이러한 오류를 방지할 수 있다. 물론 호출이 더 복잡해지긴 하지만 말이다. 첫째, LoadLibrary 함수 호출을 이용해 원하는 라이브러리를 로딩해야한다. 라이브러리의 이름으로 이 함수를 호출하면 그것을 검색 후 (발견 시) 핸들(handle)을 리턴한다:
function LoadLibrary(Name: AnsiString): TLibHandle;
다음 단계는,
function GetProcedureAddress(Lib: TLibHandle; ProcName: AnsiString): Pointer;
위를 이용해 ProcName 기호가 저장된 위치에 포인터를 가져오는데, 즉 프로시저 또는 함수의 이름 위치를 의미한다. 함수는 앞서 LoadLibrary 가 제공한 핸들을 필요로 한다.
GetProcedureAddress 는 Lib 가 참조하는 동적으로 로딩된 라이브러리에서 ProcName 기호의 위치에 포인터를 부여한다. 기호가 발견되지 않거나 핸들이 유효하지 않은 경우, 함수는 NIL을 리턴할 것이다. 윈도우에서는 명시적으로 내보내진 함수와 프로시저만 이러한 방식으로 살펴볼 수 있다. 유닉스와 같은 환경에선 내보내진 기호에 관해 질의할 때 이 방법을 사용한다:
function FreeLibrary(Lib: TLibHandle): Boolean;
여기서 언급한 함수 3개는 프리 파스칼 표준 라이브러리의 일부이며 dynlibs 유닛에 위치하는데, 해당 유닛은 사용자 애플리케이션의 uses 문에 포함되어 있어야 한다. 프로그램이 종료되면 (또는 루틴이 더 이상 필요하지 않으면) 다시 FreeLibrary를 이용해 메모리에서 라이브러리를 제거해야 한다. 언로딩(unloading)이 성공하면 해당 함수는 True를 리턴한다.
동적 로딩을 사용하려면 우리가 생성한 작은 프로그램은 다음과 같은 모습일 것이다:
program callnumbers;
{$apptype console}
{$mode delphi}
uses dynlibs; // LoadLibrary, GetProcedureAddress and FreeLibrary
type TSwitchNumbers = procedure(var A, B: Integer); cdecl; // External procedure type
var SwitchNumbers: TSwitchNumbers; // External procedure variable
A, B: Integer;
H : TLibHandle;
begin
H := LoadLibrary('numbers'); // Loads the library
if H = 0 then begin // if it wasn't found then abort
WriteLn('The library "numbers" wasn''t found!');
Halt(1);
end;
// Searches for a procedure by its name
SwitchNumbers := GetProcedureAddress(H, 'SwitchNumbers');
if @SwitchNumbers = 0 then begin // if the procedure wasn't found
WriteLn('The procedure SwitchNumbers wasn''t found in the library
"numbers"!');
FreeLibrary(H); // Release the library and quit
Halt(2);
end;
// After loading the library the program code can start:
A := 5;
B := 10;
WriteLn('Original values: A: ', A, ' B: ', B);
SwitchNumbers(A, B);
WriteLn('Are changed after calling SwitchNumbers to: A: ', A, ' B: ', B);
// Needs a WriteLn('Press [Enter] to exit');
// ReadLn; to avoid the output scrolling away too fast to be seen under Windows
FreeLibrary(H); // Release the library
end.
물론 동적 런타임 로딩을 이렇게 작은 명령 행 프로그램에서 이용하는 것은 실용적이지 않지만, 더 큰 프로그램에서 사용할 경우 여러 가지 이점이 있다. 우선 런타임에서 이용할 수 없는 외부 기능을 단일 메뉴를 회색으로 표시해버릴지도 (gray out) 모르지만 프로그램 나머지 부분의 사용을 막지는 않을 것이다. 특히 필터에 유용하다.
라이브러리는 다른 프로그램에서 사용할 수 있는 함수들을 내보내는 기능을 기본적으로 갖고 있다. 모든 애플리케이션에서 그것이 필요로 할 때마다 함수를 반복하기보다는 자주 필요로 하는 함수를 라이브러리 형태로 제공하는 편이 훨씬 실용적이다. 하지만 앞서 예제에서 보았듯 라이브러리에는 상당한 비용이 소요된다. 애플리케이션에 새 라이브러리가 필요한 경우, 그것을 찾지 못해 프로그램이 기능하지 못할 가능성이 있다. 라이브러리가 특정 운영체제에서만 이용 가능한 경우 이식성을 감소시키기도 한다. 따라서 애플리케이션 개발이 더 힘들게 된다. 하지만 우리는 “함수가 복잡할수록 라이브러리로 구현 시 더 큰 이점이 있다”는 말을 종종 한다.
동적 연결의 단점은 라이브러리가 자주 필요하지 않을 경우 프로그램 실행 속도를 늦춘다는 점이다. 따라서 우리는 애플리케이션과 연결된 코드 간 호환성을 보장하기 위해 라이브러리 버전을 추적할 필요가 있는데, 라이브러리는 시간이 지나면서 성장하여 외부 세계와의 인터페이스에 영향을 미칠 수 있기 때문이다. 새 프로퍼티를 추가한다고 해서 문제를 야기하진 않지만 내보내진 메소드를 제거하거나 파라미터를 변경하면 오래된 API를 기반으로 한 애플리케이션과 라이브러리가 호환되지 않는 일이 발생하기도 한다. 이러한 이유로 모든 동적 라이브러리에는 버전 번호가 있어야 한다. 윈도우에서 동적 라이브러리는 동적 링크 라이브러리(Dynamic Link Libraries)라고 부르며, 리눅스에선 공유 객체(Shared Objects)라 부른다. 기능은 동일하며 이름만 다르다.
운영체제 | 동적 라이브러리 접두사 | 동적 라이브러리의 확장자 | 컴파일된 정적 라이브러리의 확장자 |
Linux, FreeBSD, 등등. | lib | .so | .a |
Mac OS X (Unix Library) | lib | .dylib | .a |
Mac OS X Framework | lib | .framework (사실상Mac OS X UNIX 라이브러리가 위치한 폴더) | 프레임워크는 동적 라이브러리에만 적용 |
Windows 와 WinCE | - | .dll | .lib, .a |
표 4.4: x86 프로세서에서 프리 파스칼 호출 규칙과 효과 (라자루스에도 적용) |
유닉스와 같은 운영체제에선 라이브러리 로딩을 한 번만 허용하기 때문에 특히 버전 번호가 중요하다. 따라서 동일한 이름을 가진 라이브러리 버전을 하나 이상 사용하는 것이 불가능하다.
Linux, FreeBSD, 이와 비슷한 운영체제에선 동적 라이브러리를 아래와 같이 명명해야 한다:
<packagename>.so.<version>
예: libz.so.1 또는 libz.so.1.2.2
버전을 명시할 필요는 없지만 그러길 권한다.
MacOS X에서 라이브러리는 아래와 같이 명명해야 한다:
<packagename>.<version>.dylib
유닉스는 가장 먼저 LD_LIBRARY_PATH, 다음으로 /lib, 그 다음은 /usr/lib, 마지막으로 etc/ld.so.conf에서 발견된 경로에서 라이브러리를 검색할 것이다.
윈도우는 현재 디렉터리와 실행 파일이 위치한 디렉터리에서 라이브러리를 검색한 후 시스템 디렉터리, 이후에 PATH 환경 변수에서 발견한 위치를 대상으로 검색할 것이다. 따라서 그것을 실행해야 할 머신(machine)에 대한 전반적 구성의 영향을 받지 않고 기능할 수 있다.
단, 윈도우 체제에는 한 가지 단점이 있다. 시스템의 하드 디스크에서 이용 가능한 라이브러리의 다중 복사본이 있는데 애플리케이션 디렉터리에서 이용할 수 없는 경우, 검색 경로에서 일치하는 첫 번째 라이브러리가 사용될 것이다.
아마도 결함이 있거나 오래된 버전일지도 모른다!
프리 파스칼 자체에서 이러한 행위의 부정적 효과를 볼 수 있다. 라자루스를 사용하건 사용하지 않건, 컴파일러를 설치하고 시스템 검색 경로에 컴파일러의 바이너리 디렉터리를 포함한 경우, 다른 여러 프로그램의 작동이 중단될지도 모른다.
이는 프리 파스칼로 배포된 프로그램들 중 일부에서 Cygnus-DLL cygwin.dll을 사용하기 때문이다. 컴파일러로 배포된 라이브러리는 이 POSIX 호환성 라이브러리를 기반으로 하는 다른 프로그램들과 호환이 되기도, 되지 않기도 한다. 호환이 되지 않는다면 작동이 중지되고, 생성된 오류 메시지는 문제의 실제 원인에 대해 어떠한 실마리도 제공하지 못할 것이다.
프로그램과 라이브러리가 모두 파스칼로 작성되었다면 보통은 라이브러리의 함수 선언부를 프로그램의 소스 코드로 복사하여 extern 으로 표시하는 것만으로 충분하다. 일반 사용 또는 다른 프로그래밍 언어와 함께 라이브러리를 사용해야 한다면 호출 규칙을 고려해야 할 것이다.
기본 설정은 register 로 (표 4.4 참고), 최대 호출 속도를 야기한다. 대부분 C와 C++ 컴파일러들은 (C++ Builder 제외) 이러한 파라미터 전달 방식을 이해하지 못하므로 호출과 함께 호출 방법이 명시되어야 한다. Unix/Linux, Windows CE (Windows에서 부분적으로)에서 실행 시 cdecl 를 정의하는 편이 유용하다. 하지만 Windows에선 대개 stdcall 을 사용한다.
Unix와 같은 운영체제에서는 생성된 파일에 lib라는 접두사가 붙은 후 /usr/local/lib로 이동하는데, 프로그램을 빌드하기 전에 이를 검색하려면 지시어와 함께 링커가 제공되어야 한다. 라자루스에서는 Project→Project Options… 메뉴를 통해 해당 옵션을 이용할 수 있다.
Options dialog 창에서 Linking 페이지를 선택하고 Options: (-k) 아래에 링커로 추가 설정을 전달하는 Pass Options To The Linker (Delimiter is Space) 확인상자를 체크한다. 다음을 이용해 아래 옵션 필드를 채운다.
MacOS X에서는 생성된 라이브러리를 오른쪽 위치로 이동시키기 위해 터미널 창에 아래 명령어를 사용한다:
Unix에선 연결 도중에, 그리고 프로그램을 실행하는 도중에 라이브러리를 이용할 수 있어야 한다. Windows에선 프로그램이 실행 중일 때만 동적 라이브러리가 있으면 된다.
MacOS X에서의 라이브러리
MacOS X가 라이브러리를 처리하는 방식에 어느 정도 주의를 기울여야 한다. 가장 먼저 우리는 빌드 중 라이브러리를 운영체제 어디에 위치시킬 것인지 나타내야 한다. 이는 링커 옵션으로 처리한다.
-install_name <path>
두 번째로, 호환 버전을 명시할 필요가 있는데, 라이브러리와 호환이 되는 최소 버전 번호를 의미한다. 링커 옵션 -compatibility_version 이 이를 처리한다. 예를 들어, 이전의 4.5.x 버전과 (하지만 그 이전 버전과는 비호환) 완전히 호환되는 라이브러리 4.5.6 버전을 빌드할 경우, 호환 버전은 4.5로 설정할 수 있다. 이는 라이브러리의 오래된 버전으로 컴파일된 프로그램들이 문제없이 해당 파일로 연결할 수 있도록 해준다. 버전이 누락되면 버전 확인이 발생하지 않을 것이다. 라이브러리 버전은 아래를 이용해 나타난다:
-install_name <path>
MacOS X에서 라이브러리를 처리하는 데 발생할 수 있는 또 다른 특수 사례로 frameworks 를 들 수 있겠다. 이는 하나 또는 그 이상의 라이브러리를 포함하는 패키지이다. 프레임워크는 하위디렉터리에 바이너리 파일, include 파일, 리소스, 서브 프레임워크(sub-framework)가 있는 디렉터리 구조로, 아래 포맷으로 식별된다:
Version/<version number>, Version/<version number>/Header, Version/<version number>/Resources and Frameworks.
프레임워크의 주요 장점은 관련 라이브러리, 리소스, 헤더(header)의 그룹을 쉽게 조직화하는 방법을 제공한다는 점이다. 이는 quasi-installation 방법이다. 프레임워크는 설치 디렉터리를 휴지통으로 드래그하여 쉽게 제거할 수 있다. MacOS X엔 패키지 인스톨러는 있지만 언인스톨러는 없기 때문에 라이브러리가 MacOS X 패키지에 묶여 있다면 제거하기가 쉽지 않다. 따라서 프레임워크는 라이브러리를 관리하는 매우 편리한 해결책이다.
기본 유닉스 라이브러리는 다른 유닉스 파생 파일과 함께 /usr/lib에서 찾을 수 있으며, 컴파일 후 유닉스 프로그램에 의해 참조될 준비가 되어 있다. 하지만 Carbon과 Cocoa (비시각적 부분도 포함)와 같은 모든 시각적 및 매킨토시 지향 라이브러리들은 프레임워크로 구조화된다. 애플사에서 개발한 프레임워크는 /System/Library/Frameworks 디렉터리에 위치하는 반면 다른 프레임워크는 (물론 우리가 생성한 프레임워크도 이에 포함) /Library/Frameworks 에 보관될 것이다. 하지만 라이브러리에 속하는 include 및 리소스 파일과 바이너리 파일을 버전 디렉터리에 넣는 것으로는 충분하지 않다. 또한 프레임워크의 주 디렉터리로부터 동적 연결을 만들 필요가 있다. 대부분 라이브러리의 경우 버전 디렉터리는 관련되지 않는다.
보통은 버전 A에 관한 디렉터리를 생성하여 그 곳에 파일을 넣는 것으로 충분하다. 라이브러리 바이너리 파일은 일반적 접두사나 MacOS X용 접두사를 포함해선 안 되며, 확장자가 있어서도 안 된다.
프레임워크는 디렉터리를 /Library/Frameworks 로 단순히 드래그하면 설치된다. 보통은 프로그램 인스톨러가 이를 해결한다. 그러한 인스톨러들은 애플리케이션의 디스크 이미지에서 이용할 수 있다.
윈도우용 제어판 애플리케이션
윈도우 제어판(Windows Control Panel)은 윈도우 시스템 디렉터리에 .cpl 확장자로 된 모든 파일을 취하여 필요 시 표시하는 프로그램이다. 윈도우 시스템 디렉터리는 Windows 9x에선 SYSTEM, NT-버전에선 system32로 되어 있다. 이러한 행위는 윈도우에선 꽤 흔하다.
윈도우에 딸려 오는 rundll.exe 프로그램을 사용해서 다른 DLL을 메모리에 로딩할 수도 있다.
따라서 윈도우 제어판을 위한 Applet는 단순히 DLL라 할 수 있다. 이는 CPIApplet이란 이름의 함수를 내보낼 필요가 있다. (스펠링 정확하게!) 특정 상수와 타입도 필요하다. 애플릿은 다양한 추가 리소스를 정밀하게 정의하였을 때만 작동한다.
먼저 리소스로, 아이콘을 (ID 1000) 리소스 파일에서 이용 가능해야 하며, 애플릿의 이름과 (ID 1000) 정보 문자열도 (ID 1001) 함께 위치해야 한다. .rc 파일에서 이는 다음과 같은 모습을 할 것이다:
STRINGTABLE
1000, "CPL-Example"
1001, "Example of a Control Panel Applet"
1000 ICON "the binary Icon definition as a hexdump" ...
그러한 .rc 파일로 작업하는 것보다 더 편리한 방법은 무료 XN Resource Editor와 같은 리소스 에디터를 이용해 .res 포맷으로 리소스를 생성하는 것이다. 모든 제어판 애플리케이션은 아래와 같은 정의를 기반으로 한다:
const
WM_CPL_LAUNCH = (WM_USER+1000);
WM_CPL_LAUNCHED = (WM_USER+1001);
CPL_DYNAMIC_RES = 0;
CPL_INIT = 1;
CPL_GETCOUNT = 2;
CPL_INQUIRE = 3;
CPL_SELECT = 4;
CPL_DBLCLK = 5;
CPL_STOP = 6;
CPL_EXIT = 7;
CPL_NEWINQUIRE = 8;
CPL_STARTWPARMS = 9;
CPL_SETUP = 200;
type
PCPLInfo = ^TCPLInfo;
TCPLInfo = packed record
idIcon: Longint;
idName: Longint;
idInfo: Longint;
lData: Longint;
end;
TNewCPLInfo = packed record
dwSize : DWord;
dwFlags: DWord;
dwHelpContext: DWord;
lData: LongInt;
hIcon: HICON;
szName: array[0..31] of AnsiChar;
szInfo: array[0..63] of AnsiChar;
szHelpFile: array[0..127] of AnsiChar;
end;
이러한 엔트리들과 유사한 형태는 델파이 버전 3 이상에서 cpl.pas로 된 파일에서 찾을 수 있으며, 라자루스에서도 이용 가능하다. 위와 같은 정의를 새 라자루스 프로젝트 파일에 입력하는 것도 가능하며, 해당 실행을 위해선 File ⇒ New 에서 새 라이브러리를 생성하면 된다.
라이브러리의 가장 중요한 요소는 앞서 언급한 CPIApplet 함수인데, 이 함수는 내보내야 (exported) 한다. 해당 함수는 제어판의 요청에 응답해야 한다. 통신은 윈도우에서 일반적인 메시지 형태로 이루어진다. 따라서 함수 헤더는 꽤 복잡하다. 규칙을 전달하는 메시지는 StdCall 로 명시되어야 한다:
function CPIApplet(hwndCPL: THandle; uMsg: Cardinal;
const lParam1: Longint; var lParam2: Longint): Longint; Stdcall;
hwndCPL 변수를 사용해 우리의 창 핸들을 제어판으로 전달한다.
이 핸들은 애플릿 창을 제어판 창의 "자식"으로 사용해야 하는 경우에만 필요하다. uMsg 파라미터가 여기에 꼭 필요하다. 제어판의 확장은 이 함수만 내보내기 때문에 실행할 업무는 메시지 타입 검사로 결정되어야 한다. uMsg 내부의 메시지에 따라 lParam1 과 lParam2 파라미터들은 다른 함수를 가질 수 있다. lParam2 는 주로 전체적인 정보 집합을 애플릿으로 전달하는 포인터를 포함한다.
애플릿은 여러 단계로 구현된다. 제어판은 각 단계별로 한 번씩, 총 여러 번 애플릿을 호출할 것이다. 각 호출마다 uMsg 에 다른 메시지를 운반(carry)할 것이다. 전체 단계는 아래 순서대로 발생한다:
CPL_INIT (=1)
제어판이 시작되면 시스템 디렉터리 내 모든 .cpl 파일들이 로딩되어 (LoadLibrary) 처음으로 초기화될 것이다. 그리고 각 파일에 대해 (각 .cpl 파일) 해당 메시지로 CPIApplet 함수를 호출할 것이다. 이는 애플릿에게 초기화를 할 수 있는 기회를 제공한다 (모듈 핸들을 취득하여 이후에 필요로 하는 변수를 초기화). 다른 파라미터는 이 단계에서 사용되지 않는다. 초기화 성공 신호를 보내기 위해선 함수의 리턴값은 0이 아닌 값이어야 한다.
CPL_GETCOUNT (=2)
제어판 확장은 하나 이상의 애플릿을 포함할 수 있다 (그리고 제어판 창에 하나 이상의 아이콘을 가질 수 있다). CPL INIT 직후에 따라오는 이 메시지를 이용해 해당 .cpl 파일 내 애플릿 수를 리턴할 수 있다. 다른 파라미터는 사용되지 않는다.
CPL_INQUIRE (=3)
이는 제어판의 시작 시 전달되는 세 번째 메시지로, 애플릿에 관한 (기호와 이름) 일부 정보를 요청한다. lParam1 은 질의한 애플릿의 수에 관한 정보를 포함한다 (0부터 계수 시작). lParam2 는 데이터가 보관되어야 하는 TCPLInfo 타입의 데이터 구조에 대한 포인터를 포함한다. 완성이 성공되었음을 신호로 보내기 위해선 함수 결과가 0이어야 한다. 델파이에서 정보 레코드는 정수로 정의된다. 라자루스에선 LongInt 를 사용하는 편이 나은데, 프리 파스칼로 정의 시 정수가 종종 16 비트로만 이루어진 엔트리로 정의되기 때문이다.
tagCPLINFO = packed record
idIcon: Longint; // icon resource id
idName: Longint; // name string resource id
idInfo: Longint; // info string res. id
lData : Longint; // user defined data
end;
TCPLInfo = packed record
idlcon 필드는 리소스가 정의한 아이콘의 식별 번호를 포함하는데, 이 번호는 제어판에 표시되어야 한다. idName 은 문자열 리소스를 의미하며, 아이콘 아래 표시되어야 할 이름을 포함한다.
제어판 창에는 현재 활성화된 기호에 관한 추가 정보를 보여주는 상태줄이 있다. idInfo 필드는 이러한 정보를 포함하는 문자열 리소스를 참조한다. lData 는 자유롭게 사용할 수 있는 integer variable(정수형 변수) 이다. 이는 CPL_DBLCLK 메시지에 다시 나타날 것이다. 따라서 실제 정보는 직접적으로 전달되지 않으며, .cpl 라이브러리 내 리소스로의 참조에 의한 전달로만 이루어진다. 실제 데이터를 얻는 것은 제어판이 책임진다.
CPL_NEWINQUIRE (=3)
이 메시지는 CPL_INQUIRE과 함께 전송된다. 동일한 함수를 가지지만 리턴된 데이터 구조는 (TNewCPLInfo 타입) 더 크다. CPL_NEWINQUIRE는 윈도우 3 세대에서 내려온 것이다. Win32 API 문서에서는 CPL_INQUIRE가 "캐시 처리 가능"하다는 (성능 향상) 이유로 그것을 사용하도록 권한다. 따라서 이 메시지에 대해 0이 아닌 리턴값만 리턴하면 되는데, 이는 제어판에서 무시할 것이다. CPL_INQUIRE과 CPL_NEWINQUIRE 메시지 둘 다 제어판 시작 시와 애플릿이 활성화되는 순간에 발생한다 (CPL_DBLCLK 메시지 참고).
CPL_SELECT (=4)
이 메시지 또한 오래 전부터 계속 사용해왔다. Win32에선 전달되지 않으므로 여기서 다룰 필요가 없겠다.
CPL_DBLCLK (=5)
사용자가 제어판에서 애플릿 기호를 더블클릭하여 애플릿을 활성화하면 이 메시지가 CPIApplet 를 호출한다. 모드형(modal) 대화창이 생성되어 사용자가 애플릿과 상호작용하도록 해준다. lParam1 은 애플릿의 수를 포함하고 (0부터 계수 시작), lParam2 는 전달된 사용자 정의의 데이터 값을 TCPLInfo 의 lData 에 포함한다 (CPL_INQUIRE 메시지 참고). 성공적으로 완성되었음을 신호로 보내려면 0을 결과값으로 리턴한다. 이 메시지가 한 번 이상 도착하도록 애플릿을 준비해야 한다. 모드형 대화창이 열려 있는 한 메시지는 애플릿으로 전달되지 않을 것이며, 심지어 사용자가 아이콘을 다시 더블 클릭 하더라도 마찬가지다.
CPL_STOP (=6)
이제 거의 끝나간다. 애플릿 닫기가 (모드형 대화창) 감지되었음을 알리기 위해 제어판은 .cpl 확장자로 메시지를 전송한다. lParam1과 lParam2는 CPL_DBLCLK에서와 마찬가지로 애플릿의 수와 lData 값을 포함한다. 메시지 도착을 알리기 위한 함수 결과는 0이다.
CPL_EXIT (=7)
이 메시지는 애플릿이 닫힌 후에 나타난다. 파라미터는 사용되지 않는다. 함수 결과는 0이어야 한다. 이러한 함수들의 구현은 델파이에서도 몇 년째 이용되어왔다. 단, 라자루스로는 부분적으로만 이동이 가능하다.
델파이에선 애플리케이션 오브젝트를 항상 이용할 수 있는 반면 라자루스에선 명시적으로 선언해야 한다는 점이 주요 차이점이다.
아래는 완전한 제어판 애플릿을 위한 소스 코드이다
library Bspl;
{$mode Delphi}{$H+}
uses Interfaces, // this includes the LCL widgetset
Forms, Unit1, LResources, Dialogs, Windows
{ you can add units after this };
($IFDEF WINDOWS}($R bspl.rc}{$ENDIF}
($R CPLDATAI.RES} // External resources with Icon, Name and text information
// here it integrates the data which would go in a Unit
const WM_CPL_LAUNCH = (WM_USER+1000);
WM_CPL_LAUNCHED = (WM_USER+1001);
CPL_DYNAMIC_RES = 0;
CPL_INIT = 1;
CPL_GETCOUNT = 2;
CPL_INQUIRE = 3;
CPL_SELECT = 4;
CPL_DBLCLK = 5;
CPL_STOP = 6;
CPL_EXIT = 7;
CPL_NEWINQUIRE = 8;
CPL_STARTWPARMS = 9;
CPL_SETUP = 200;
type PCPLInfo = ^TCPLInfo;
TCPLInfo = packed record
idIcon : Longint;
idName : LongInt;
idInfo : Longint;
lData : Longint;
end;
TNewCPLInfo = packed record
dwSize : DWord;
dwFlags : DWord;
dwHelpContext : DWord;
lData : Longint;
hIcon : HICON;
szName : array[0..31] of AnsiChar;
szInfo : array[0..63] of AnsiChar;
szHelpFile : array[0..127] of AnsiChar;
end;
function CPIApplet(hwndCPL: THandle; uMsg: Cardinal;
const lParam1: Longint; var lParam2: Longint): Longint; Stdcall;
// The exported system control function
var CPLInfo: TCPLInfo absolute lParam2;
AppInitcalled: Boolean;
begin
Result := 1;
AppInitcalled := False;
case uMsg of // handling of uMsg
CPL_INIT: Result := 1; // initialization
CPL_GETCOUNT: Result := 1; // number of applets in this example
CPL_INQUIRE: begin // query for icon, name and info
if lParam1 = 0 then
begin
with CPLInfo do
begin
idIcon := 1000; // ResourceID of the Icon in CPLDATAI.RES
idName := 1000; // ResourceID of the Name in CPLDATAI.RES
idInfo := 1001; // ResourceID of the text info in CPLDATAI.RES
lData := 1234; // freely definable value
end;
Result := 0;
end
else
Result := 1; // error (nonexistent applet)
end;
CPL_NEWINQUIRE: Result := 1; // this message will be ignored
CPL_DBLCLK:
begin // start applet
if not lParam1 = 0 then
begin
if not AppInitCalled then
begin
Application.Initialize; // vital for Lazarus!
AppInitCalled := True; // only initialize once
end;
if Form1 = NIL then
Form1 := TForm1.Create(Application); // Lazarus and Delphi
Application.UpdateMainform(Form1); // Lzarus specific
Form1.ShowModal; // Show is not enough
Form1.Free; // free the window
Form1 := NIL; // and set it to NIL
Result := 0;
end else Result:=1; // error (nonexistent applet)
end;
CPL_STOP: Result := 0; // windows has been closed
CPL_EXIT: Result := 0; // DLL is released
end; // Case
end;
exports
CPIApplet;
end.
하나의 애플릿만 포함하는 제어판 모듈의 경우 필요한 것은 이것밖에 없다.
델파이와 차이가 매우 분명하다.
CPL_DBLCLK: begin // start applet
if lParam1 = 0 then
begin
Form1 := TForm1.Create(Application);
Form1.ShowModal;
Form1.Free;
Result := 0;
end
else Result := 1;
end;
위의 리스트에서 볼 수 있듯이 위를 이용해 델파이에서 가능한 작업이 라자루스에선 훨씬 더 많은 노력을 요한다. 하지만 결과는 동일하다: 애플릿이 호출되자마자 메인 창 Form1이 열릴 것이다. 하나 이상의 애플릿을 구현해야 할 경우 코드 일부는 더 복잡해진다. 먼저 CPL_GETCOUNT와 CPL_INQUIRE는:
CPL_GETCOUNT : Result := 2; // here we now have two applets
CPL_INQUIRE : begin // query for icon, name and info
if lParam1 = 0 then
begin // first applet
with CPLInfo do
begin
idIcon := 1000; // Icon resource ID
idName := 1000; // Name resource ID
idInfo := 1001; // text information resource ID
lData := 1234; // freely definable value
end;
Result := 0; //
end
else
if lParam1= 1 then
begin // second applet
with CPLInfo do
begin
idIcon := 2000; // icon resource ID
idName := 2000; // name resource ID
idInfo := 2001; // text information resource ID
lData := 4321; // freely definable value
end;
Result:= 0;
end
else Result := 1; // errorーmissing applet
end;
물론 원하는 대로 확장할 수도 있다. 예제에서 볼 수 있듯, 리소스에 대한 식별자는 정의가 간단하다.
비슷한 방식으로 애플릿을 호출하기 위한 실제 호출에 해당하는 블록을 두 배로 할 (double) 수 있다:
CPL_DBLCLK: begin // start applet
if lParam1 = 0 then
begin // first applet
if not AppInitCalled then
begin
Application.Initialize; // vital for Lazarus!
AppInitCalled := True; // only initialize once
end;
if Form1 = NIL then
Form1 := TForm1.Create(Application); // Lazarus and Delphi
Application.UpdateMainform(Form1); // Lazarus specific
Form1.ShowModal; // show is not enough
Form1.Free; // free the window
Form1 := NIL; // ... and set it to NIL
Result := 0;
end // first applet
else
if lParam1 = 1 then
begin // second applet
if not AppInitCalled then
begin
Application.Initialize; // important in Lazarus!
AppInitcalled := true // only initialize once
end;
if Form2 = NIL then // Lazarus and Delphi
Form2 := TForm2.Create(Application);
Application.UpdateMainform(Form2); // Lazarus specific
Form2.ShowModal; // Show is not enough
Form2.Free; // free the window
Form2 := NIL; // ... and set it to NIL
Result := 0;
end // second applet
else
Result := 1; // errorーnot available applet
end; // start applet
제어판 applets 는 두 가지 점에서 문제가 된다. 첫 번째 문제는 applet 자체의 개발이다: DLL은 라자루스에서 시작 및 테스트할 수 없다. 따라서 우리에겐 두 개의 프로젝트가 필요하다: 하나는 프로그램으로부터 창을 정상적으로 여는 데 필요하다. 그리고 나면 메인 창이 라이브러리 프로젝트로 전달된다. 이 프로젝트는 IDE에서 올바른 대상 이름과 .cpl 확장자를 얻는다. 이는 Project ⇒ Project Options 로 가서 Compiler Options 헤드에서 Paths를 선택한 후 Target file name (-o): 에디트 상자에 원하는 이름을 입력한다 (예: my_first_applet.cpl). 두 번째 문제는 좀 더 까다롭다: 윈도우는 부분적으로 제어판 애플릿을 차단(block)한다. 이는 현재 디렉터리를 더블 클릭하여 시작 가능하지만 이 방법을 이용 시 제어판에서 호출할 수 없다.
하지만 C:\Windows\System32 로 복사할 경우 윈도우에서 차단하기 때문에 다시 삭제할 수 없으며, 이를 제거할 수 있는 절차를 원할 것이다. 우리는 언인스톨 루틴을 쉽게 작성할 수 있다. 이는 배치 파일(batch file)로 구성되는데, 파일은 C:\Programs 등 어떤 이름의 디렉터리든 위치할 수 있다. 해당 rmfile.bat 은 아래와 같은 행만 포함한다.
@del C:\WINDOWS\system32\My_First_Applet.cpl
(혹은 다른 어떤 파일 이름이든). 해당 파일은 RegEdit을 이용해 RunOnce으로 레지스트리에 입력된다. 실행을 위해선 RegEdit를 열고 RunOnce를 검색한다. 그리고 문자열에 대한 새 시퀀스, 즉 인스턴스 rmfile의 경우 C:\Programs\rmfile.bat 값을 입력한다. 이는 윈도우로 하여금 시작 시, 즉 활성화되기 전에 CPL 파일을 삭제하도록 명령한다. RunOnce를 선택하여 한 번만 실행되도록 보장한다. 물론 그러한 시스템 파일을 운영체제로 삽입하여 다시 제거하기에 적절한 인스톨러를 사용하는 편이 더 낫다. 작은 프로그램을 다룬다면 자체 생성된 .inf 파일만으로 충분할 것이다.