Custom Build

admin의 아바타

이 강좌는 어느정도의 C언어 지식과, Makefile의 지식이 필요합니다.

Tcl/Tk에서 현재 많이 사용되는 빌드방법은 세가지입니다.

  • Tclkit
  • Freewrap
  • Custom build

Tclkit, Freewrap은 동적 라이브러리와 유저 소스 코드를 하나의 실행파일안에 넣어 빌드하기 때문에, vtk 확장패키지 같은 대량의 동적 라이브러리를 사용하는 프로그램에는 적합하지 않습니다. 하지만, Tclkit이나 Freewrap은 코드 암호화 측면이나 사용하기 쉬운 빌드측면에서 자주 사용되는 방법이기도 합니다.

이번 강좌는 Tcl/Tk를 이용하여 개발된 상용 어플리케이션에서 대부분 채택된 방법인 Custom Build에 대해서 알아봅니다. Custom Build는 앞의 단점들을 능력에 따라 대처할 수 있습니다.

Custom Build의 목표는 Tcl/Tk 베이스 방식인 wish, Tcl 방식인 tclsh를 만드는 작업입니다. 역으로 생각하면 Tcl 인터프리터 생성부터 코드의 실행 루틴까지 모든 처리를 작성해줘야 한다는 말이 됩니다. 이를 만들면서 tclsh와 wish의 동작원리에 대해 알 수 있을것입니다.

강좌 시작전 우리가 나아갈 최종 목표의 결과물을 확인해 봅시다.
첨부파일의 test.7z 을 받아서 test.exe를 실행해보시면, Tcl/Tk가 설치되어 있지 않아도 실행이 됨을 볼수 있습니다. :-)

Tcl 소스코드 준비

아래의 Tcl 소스코드를 준비합니다.

package require tile

ttk::button .b -text "hello world!!" -command exit
pack .b

코드 준비

test.cpp

이 코드는 Tcl/Tk 베이스의 wish.exe와 같은 기능을 담당하는 코드입니다. 여기서는 test.exe를 생성합니다. 자세한 Tcl API설명은 피하며, Tcl API Reference를 보시면 알 수 있습니다.

#include <tcl.h>
#include <tk.h>

#include <windows.h>

#include <errno.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>

#ifndef MAX_PATH
#define MAX_PATH        2048
#endif

char **argv;
int argc;

void Debug (char *string) {
        MessageBox(NULL, string, "Debug", MB_OK|MB_ICONINFORMATION);
}

int memerror() {
        fprintf(stderr, "Can't allocate enough memory. Bailing out\n");
        exit(1);
}

static void setargv(int *argcPtr, char ***argvPtr) {
    char *cmdLine, *p, *arg, *argSpace;
    char **argv;
    int argc, size, inquote, copy, slashes;
   
    cmdLine = GetCommandLine();

    size = 2;
    for (p = cmdLine; *p != '\0'; p++) {
        if ((*p == ' ') || (*p == '\t')) {
            size++;
            while ((*p == ' ') || (*p == '\t')) {
                p++;
            }
            if (*p == '\0') {
                break;
            }
        }
    }
    argSpace = (char *) Tcl_Alloc(
            (unsigned) (size * sizeof(char *) + strlen(cmdLine) + 1));
    argv = (char **) argSpace;
    argSpace += size * sizeof(char *);
    size--;

    p = cmdLine;
        for (argc = 0; argc < size; argc++) {
                argv[argc] = arg = argSpace;
                while ((*p == ' ') || (*p == '\t')) {
                        p++;
                }
                if (*p == '\0') {
                        break;
                }

                inquote = 0;
                slashes = 0;
                while (1) {
                        copy = 1;
                        while (*p == '\\') {
                                slashes++;
                                p++;
                        }
                        if (*p == '"') {
                                if ((slashes & 1) == 0) {
                                        copy = 0;
                                        if ((inquote) && (p[1] == '"')) {
                                                p++;
                                                copy = 1;
                                        } else {
                                                inquote = !inquote;
                                        }
                                }
                                slashes >>= 1;
                        }

                        while (slashes) {
                                *arg = '\\';
                                arg++;
                                slashes--;
                        }

                        if ((*p == '\0') || (!inquote && ((*p == ' ') || (*p == '\t')))) {
                                break;
                        }
                        if (copy != 0) {
                                *arg = *p;
                                arg++;
                        }
                        p++;
                }
                *arg = '\0';
                argSpace = arg + 1;
        }
    argv[argc] = NULL;

    *argcPtr = argc;
    *argvPtr = argv;
}

int Init(Tcl_Interp* interp) {
    char *args;
    char buf[100];
    char script[1024];
    char tcl_dir [1024];
    char tk_dir [1024];
    char autopath [1024];
    char mainfile [1024];
    char prgpath [1024];
    char *p;

    char drv[MAX_PATH];
    char path[MAX_PATH];
    char fname[MAX_PATH];
    char ext[MAX_PATH];

    Tcl_FindExecutable(argv[0]);
    interp = Tcl_CreateInterp();

    if( Tcl_PkgRequire(interp, "Tcl", TCL_VERSION, 1)==0 ){
            return 1;
    }

    args = Tcl_Merge(argc-1, (CONST84 char * CONST *)argv+1);
    Tcl_SetVar(interp, "argv", args, TCL_GLOBAL_ONLY);
    ckfree(args);
    sprintf(buf, "%d", argc-1);
    Tcl_SetVar(interp, "argc", buf, TCL_GLOBAL_ONLY);
    Tcl_SetVar(interp, "argv0", argv[0], TCL_GLOBAL_ONLY);
    Tcl_SetVar(interp, "tcl_interactive", "0", TCL_GLOBAL_ONLY);

     GetModuleFileName(NULL, mainfile, 1024);
     _splitpath(mainfile, drv, path, fname, ext);
        sprintf(prgpath, "%s%s", drv, path);
       
    sprintf(tcl_dir, "%s/tcl8.4", prgpath);
    sprintf(tk_dir, "%s/tk8.4", prgpath);

    Tcl_SetVar2(interp, "env", "TCL_LIBRARY", tcl_dir, TCL_GLOBAL_ONLY);
    Tcl_SetVar2(interp, "env", "TK_LIBRARY", tk_dir, TCL_GLOBAL_ONLY);

    if( Tcl_Init(interp) ) return TCL_ERROR;

    sprintf(autopath, " %s", tcl_dir);
    Tcl_SetVar(interp, "auto_path", autopath, TCL_GLOBAL_ONLY|TCL_APPEND_VALUE);
   
    Tcl_SetVar(interp, "tcl_libPath", tcl_dir, TCL_GLOBAL_ONLY);

    Tk_InitConsoleChannels(interp);
    if ( Tk_Init(interp) ) return TCL_ERROR;

    Tcl_StaticPackage(interp,"Tk", Tk_Init, 0);
    Tk_CreateConsoleWindow(interp);

    sprintf(script, "%stest.tcl", prgpath);
    Tcl_EvalFile(interp, script);

    Tk_MainLoop();

    Tcl_DeleteInterp(interp);
    Tcl_Exit(0);

    return TCL_OK;
}

int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance,
  LPSTR lpCmdLine, int nCmdShow )
{
    setargv(&argc, &argv);
   
    Tk_Main(argc, argv, Init);

    return 0;
}

test.rc

리소스 파일을 아래와 같이 준비합니다. 아래와 같이 test.exe의 아이콘으로 사용됩니다.

MYICON  ICON    "test.ico"

리소스 파일에 명시되어 있듯이, test.ico 아이콘 파일이 준비되어야합니다. 여기서는 tclsh.ico를 raname하여 사용합니다.

Makefile

빌드를 하기위한 Make룰 파일입니다. TCLPATH 변수를 수정할 필요가 있습니다.

LINK = link
TCLPATH = C:\Tcl84

all: test.exe

test.exe: test.obj test.res
        link.exe /NODEFAULTLIB:MSVCRT \
                /SUBSYSTEM:WINDOWS /OUT:test.exe \
                test.obj test.res \
                "$(TCLPATH)\lib\tcl84.lib" \
                "$(TCLPATH)\lib\tk84.lib" \
                netapi32.lib comctl32.lib ws2_32.lib \
                advapi32.lib user32.lib gdi32.lib \
                comdlg32.lib winspool.lib oldnames.lib \
                ole32.lib shell32.lib uuid.lib imm32.lib \
                comctl32.lib netapi32.lib

test.obj: test.cpp
        cl /c /O test.cpp \
                /I "$(TCLPATH)\include"

test.res: test.rc
        rc test.rc

clean:
        -@erase "*.obj"
        -@erase "*.exe"
        -@erase "*.res"

빌드하기

Makefile을 바탕으로 test.exe를 생성해봅니다. 윈도우즈 커맨드 창에서 아래와 같이 입력합니다.

vcvars32
nmake /f Makefile.vc

에러가 없이 빌드가 되었다면, test.exe가 생성이 될 것입니다. 이것으로 Custom Build에 있어서 핵심적인 실행파일 test.exe 생성작업은 끝났습니다.

파일 준비

test.exe 만으로는 우리의 코드인 test.tcl을 실행할 수 없는데, 이를 위해서는 추가적으로 여러개의 파일을 준비해야합니다.
아래의 복사할 파일들은 test.exe가 위치해 있는 디렉토리를 기준으로 합니다. 또한 파일이나 디렉토리에 명시된 버전은 시스템에 따라 다를수 있습니다.

Tcl/Tk 코어 파일

Tcl/Tk 코어 파일인 tcl84.dll, tk84.dll을 복사합니다. 또한 Tcl 라이브러리, Tk 라이브러리 디렉토리를 복사합니다.

확장 패키지 준비

사용된 test.tcl 예제에서는 tile 확장 패키지가 사용되었으므로, tile0.8.2 디렉토리를 복사합니다. 예로 vtk가 사용되었다면 test.exe가 있는 위치에 vtk관련 dll과 확장 패키지 디렉토리리들을 복사합니다.

이것으로 Custom Build의 최종 구성은 다음과 같습니다.

마지막으로

지금까지 알아본 Custom Build는 소스 코드가 공개되는 단점이 있습니다. 물론 직접 소스코드 암호화/복호화 함수를 구현하는 방법이 있겠으나, 이미 구현되어 있는 tclcompiler와 tbcload를 이용하는 방법도 있습니다. tclcompiler로 소스 코드를 암호화 하였다면, 확장 패키지 준비 단계에서 복호화 하기위한 tbcload 확장 패키지를 복사하고, test.cpp 코드내의 test.tcl 파일을 컴파일된(암호화된 test.tbc) 파일로 변경해 주시면 됩니다.

마지막으로 위의 모든 소스를 첨부합니다. source.7z 을 다운받으세요.

이 강좌의 내용은 수정및 보완 될 수 있습니다.

첨부 파일파일 크기
hello.png5.23 KB
test.7z1.09 MB
resource.png643 bytes
build.png10.75 KB
structure.png2.78 KB
source.7z4.55 KB