델파이에서 C 오브젝트 파일 사용하기
(Using C object files in Delphi)
번역 : 김태성 (jsdkts@korea.com) 2007.10.31
제 마음대로 번역이니 조금 엉망이어도 양해 바랍니다.
C는 매우 광범위하게 사용되어지는 언어인데, 세계적으로 만들어진 많은 라이브러리가 C 코드로 되어 있다. 하지만, 델파이 코드 라이브러리는 이에 비해서는 매우 적다. 그래서 많은 C 라이브러리를 델파이 코드로 일일이 변환하지 않고 사용할 수 있다면 좋을 것이다. 다행히도 델파이는 C 오브젝 파일 링크를 허용하고 있다. 그러나, C 오브젝 파일 링크시 "외부명칭 참조불가"라는 문제에 부딪힌다.
C는 단순하지만 강력한 언어로 런타임 라이브러리에서 많은 기능을 가져온다. 대부분의 C코드는 이 라이브러리의 몇가지 함수를 필요로 한다. 그러나 델파이 런타임에는 이러한 함수가 없다. 그래서 간단히 C 오브젝 파일을 링킹하는 것은 "외부명칭 참조불가" 에러를 낸다. 운 좋게도 C는 어떤 외부 함수의 구현을 허용하는데, 외부 함수를 정의해 사용하는데 문제는 없다. 링크가 필요로 하는 함수명이 있다면 그것을 먼저 쓰기 때문이다. 그래서 델파이 코드에서 필요한 함수 코드를 준비해 주면 되는 것이다.
이 기사에서 난 델파이 유닛에 C 오브젝파일을 어떻게 컴파일하고 링크하는지, 필요한 C 런타임의 참조함수를 어떻게 준비하는지 데모로 보여줄 것이다. 이를 위해 잘 알려진 토론토 대학의 Henry Spencer에 의해 쓰여진 도메인 정규식 검색 코드를 사용할 것이다. 이 코드는 볼랜드 C++로 컴파일 되도록 약간 수정 했을 뿐이다. 정규식은 델파이 도움말에 약간 나와 있는데 패턴 검색을 멋지게 정의하는 방법이다.
오브젝트 파일(Object files)
C는 실행파일 링크를 위한 보통의 오브젝 파일을 생성하는데, 32비트 윈도에서 보통 .obj 확장자를 사용한다. 그러나 obj는 MS C++ 컴파일러의 COFF 포멧과 COFF 포멧을 약간 수정한 다른 컴파일러의 포멧이 다르다. 이는 델파이에서 사용할 수 없다. 델파이는 볼랜드의 OMF 포멧의 obj 파일만 가능하다. COFF 포멧을 OMF 포멧으로 변환하는 방법은 가능하지 않다. 그래서 소스가 필요하며 OMF 파일로 컴파일해서 생성해야 한다.
Note: COFF2OMF 유틸리티는 C++빌더의 모든 버전에 있는데, 이 문제에 대한 도움은 없다. 다만 import 라이브러리를 하나의 포멧에서 다른 것으로 변환하는 것만 가능하다. Import 라이브러리는 DLL의 export 함수 정보를 담고 있고, 이는 ImpLib나 이와 유사한 유틸리티를 사용해 DLL에서 직접 생성된다. 여기엔 C/C++ 라이브러리 파일이 담고 있는 것에 비해 매우 제한적인 정보를 담고 있다. COFF2OMF는 COFF포멧의 C/C++ 오브젝 또는 실제 코드가 있는 라이브러리 파일을 변환하지 못한다. 그래서 소스 파일이 필요하고 볼랜드 컴파일러로 델파이에서 사용되어질 OMF 오브젝 파일을 생성해야 한다.
UPDATE: Thaddy de Koning은 내게 DigitalMars의 COFF2OMF 변환기는 완벽히 변환 가능하다고 말했다. 난 시도해 보지 않았지만 그의 말은 진짜 같다. 뜻있는 분들은 시도해 보시기를 바란다. http://www.digitalmars.com/eup.html
Borland/CodeGear의 C++ Builder는 OMF 오브젝 파일을 생성한다. 하지만 델파이 사용자에겐 이 개발툴이 없는 경우가 많다. 다행히도 볼랜드는 Borland C++ Builder 버전5 커맨드 라인 컴파일러를 프리로 공개했다. http://www.codegear.com/downloads/free/cppbuilder 링크에서 다운 받을 수 있다. Borland는 이미 버전 6를 릴리즈 했는데, 익숙해질때 까지 버전5를 사용하는게 좋다.
UPDATE: Borland/CodeGear는 Turbo C++ Explorer라는 공개용 컴파일러를 릴리즈 했다. 비록 이름은 같지만 이전 20세기의 Borland/Turbo C++ 제품과는 다른데, BDS 2006 속의 C++ 제품의 축소판이다. 이 제품은 완전한 IDE를 갖추고 있으며 상업용 개발도 허용하고 있다. 단지 새로운 컴포넌트 설치 기능만 안된다. 그러나 여전히 코드 속에서 컴포넌트의 사용은 가능하다. 다운로드는 http://www.turboexplorer.com/downloads
여기에는 당신이 사용하는 파일의 종류에 대한 또 다른 제한이 있다. C++이 아닌 C 파일로 컴파일되어진 C 오브젝 파일만 사용 가능하다는 점이다. 이는 델파이 링커가 C++ 오브젝 파일에 대한 처리 문제를 안고 있기 때문이다. 그래서 소스 파일은 .cpp 가 아닌 .c 확장자를 가져야만 한다. 하지만 C++ 클래스를 직접 사용하지 않는다면 이는 심각한 제한은 아닌 것이다.
One note: C는 종종 .lib 라이브러리를 사용 한다. lib는 오브젝 파일이 여러개 포함된
것이다. 어떤 C 컴파일러는 라이브러리 프로그램을 가지고 있는데 추출, 삽입, 대체 또는 오브젝 파일 리스트를 보여주는 일을 한다.
델파이에서는 .lib 파일의 직접 링크가 가능치 않다. 그러나 TDUMP 유틸리티를 써서 델파이와 C++빌더가 lib 속에 무엇을
채웠는지 볼수 있다. 프리 C++ 컴파일러에는 TLIB도 포함되어 있다.
코드 (The code)
나는 여기서 메커니즘이나 정규식 사용에 대해 논하지 않겠다. 그건 책이나 인터넷 속의 정보로도 충분하다. 하지만 이 코드의 공헌은,
검색 코드로 번역이 쉽게 되도록 문구 표현을 바꾸는 초간단 컴파일러와 같은 종류와 같이 패턴 정규식을 해석하는 것이다. 해석은
recompile() 함수로 종결 된다. 스트링 정규식 패턴 검색을 위해서는, 패턴과 스트링을 regexec() 함수로 컴파일 하는데,
이건 스트링 속에 패턴이 매칭되는 텍스트가 어디 있는지 정보를 리턴할 것이다.
정규식 검색의 완전한 코드 구현은 훨씬 복잡하고 길다. 그걸 여기서는 보여줄 수 없다. 그러나 헤더 파일은 오브젝 파일을 사용하는 델파이 코드를 위한 것으로 중요하다.
Download /***************************************************************************/ /* */ /* regexp.h */ /* */ /* Copyright (c) 1986 by Univerisity of Toronto */ /* */ /* This public domain file was originally written by Henry Spencer for the */ /* University of Toronto and was modified and reformatted by Rudy Velthuis */ /* for use with Borland C++ Builder 5. */ /* */ /***************************************************************************/ #ifndef REGEXP_H #define REGEXP_H #define RE_OK 0 #define RE_NOTFOUND 1 #define RE_INVALIDPARAMETER 2 #define RE_EXPRESSIONTOOBIG 3 #define RE_OUTOFMEMORY 4 #define RE_TOOMANYSUBEXPS 5 #define RE_UNMATCHEDPARENS 6 #define RE_INVALIDREPEAT 7 #define RE_NESTEDREPEAT 8 #define RE_INVALIDRANGE 9 #define RE_UNMATCHEDBRACKET 10 #define RE_TRAILINGBACKSLASH 11 #define RE_INTERNAL 20 #define RE_NOPROG 30 #define RE_NOSTRING 31 #define RE_NOMAGIC 32 #define RE_NOMATCH 33 #define RE_NOEND 34 #define RE_INVALIDHANDLE 99 #define NSUBEXP 10 /* * The first byte of the regexp internal "program" is actually this magic * number; the start node begins in the second byte. */ #define MAGIC 0234 #pragma pack(push, 1) typedef struct regexp { char *startp[NSUBEXP]; char *endp[NSUBEXP]; char regstart; /* Internal use only. */ char reganch; /* Internal use only. */ char *regmust; /* Internal use only. */ int regmlen; /* Internal use only. */ char program[1]; /* Internal use only. */ } regexp; #ifdef __cplusplus extern "C" { #endif extern int regerror; extern regexp *regcomp(char *exp); extern int regexec(register regexp* prog, register char *string); extern int reggeterror(void); extern void regseterror(int err); extern void regdump(regexp *exp); #ifdef __cplusplus } #endif #pragma pack(pop) #endif // REGEXP_H |
위의 헤더는 몇개의 상수값과, 정규식과 호출자 사이의 정보를 전하는 구조체로 정의 되어 있고, 코드의 다른 함수와 사용자 호출 함수로 되어 있다.
#define 값은 RE_ 로 시작되는데 함수 호출후 성공 또는 실패 값을 돌릴때 쓰는 상수 값이다. NSUBEXP는 정규식이 실행될 때 가지는 부표현식의 숫자이다. MAGIC 값은 정규식이 각각 컴파일 되어진 것에 있어야 하는 값이다. 만일 없다면, 구조체는 분명하게 바른 컴파일 값을 담지 못할 것이다. 0234는 10진수가 아니다. 0이 앞에 오면 C 컴파일러는 8진수로 인식한다.
0234(oct) = 2 * 82 + 3 * 81 + 4 * 80 = 128 + 24 + 4 = 156(dec).
#pragma pack(push,1)는 현재 바이트정렬 값을 보관하고 1 바이트정렬로 설정한다. #pragma pack(pop)은 이전의
설정 값으로 되돌린다. 이는 구조체를 델파이의 packed record 와 호환되게 만들기 때문에 아주 중요하다.
코드 컴파일(Compiling the code)
C++ Builder나 BDS2006를 가지고 있다면, 코드 컴파일은 매우 쉽다. 새로운 프로젝트를 생성한 후 프로젝트에 "regexp.c"를 추가하고 프로젝트를 컴파일 하면 된다. 그러면 디렉토리에 "regexp.obj" 파일을 얻을 수 있다.
설정이 제대로 된 커맨드라인 컴파일러를 가지고 있다면, 커맨드 창을 열어 "regexp.c" 파일이 있는 디렉토리로 가서 다음과 같이 입력한다.
bcc32 -c regexp.c
아마도 미사용 변수와 정수 형변환에 대한 경고가 있으나 간단히 무시한다. 난 이 코드를 아무런 문제없이 이미 1년간 사용해 왔다. 컴파일이 잘 되면 소스와 같은 디렉토리에 "regexp.obj" 파일을 볼수 있을 것이다.
델파이에 오브젝 파일을 import하기 위해, 이 오브젝 파일을 델파이 소스가 있는 폴더로 복사해야 한다.
오브젝 파일 Import (Importing the object file)
오브젝 파일에 있는 코드를 사용하려면, 몇가지 선언을 해야 한다. 델파이 링커는 "regexp.h" 헤더에 있는 regexp 관련 함수의 파라메터와 정의된 값을 알수 없다. 또한 어떤 호출 규약을 사용했는지도 알 수 없다. 그래서 이를 위해 Import 유닛을 작성해야 한다.
여기에 C 오브젝 파일에 있는 함수와 헤더에 있는 값을 쓸수 있도록 델파이 유닛으로 된 인터페이스가 있다.
unit RegExpObj; interface const NSUBEXP = 10; // The first byte of the regexp internal "program" is actually this magic // number; the start node begins in the second byte. MAGIC = 156; type PRegExp = ^_RegExp; _RegExp = packed record StartP: array[0..NSUBEXP - 1] of PChar; EndP: array[0..NSUBEXP - 1] of PChar; RegStart: Char; // Internal use only. RegAnch: Char; // Internal use only. RegMust: PChar; // Internal use only. RegMLen: Integer; // Internal use only. Prog: array[0..0] of Char; // Internal use only. end; function _regcomp(exp: PChar): PRegExp; cdecl; function _regexec(prog: PRegExp; str: PChar): LongBool; cdecl; function _reggeterror: Integer; cdecl; procedure _regseterror(Err: Integer); cdecl; |
모든 함수는 함수명 앞에 _ 언더바를 가지고 있음에 주의하라. 이는 역사적인 이유가 있다. 대부분의 C 컴파일러는 여전히 C 함수의 이름 앞에 _ 를 붙이고 있다. 이를 Import 하기 위해서 이름 앞에 _ 를 붙이는 것이다. C++Builder는 앞에 _ 를 생략한다고 말하는 사람도 있는데, 난 그렇지 않다고 생각한다. 언더바는 C 함수를 사용한다는 것을 명확히 보여 주는 의미로 보면 된다. 이는 델파이 문법으로 cdecl 이라 부르는 C 함수 호출규약으로 선언 되어져야 한다. 이를 잊으면 찾기 어려운 버그를 만들 것이다.
역자주:
(구조체의 경우는 obj 파일에 저장되지 않으므로 명칭 앞에 _ 를 붙일 이유가 없으나, 원저자가 명칭의 일관성을 위해 붙인 것으로
보인다. 구조체도 실체적인 변수 인스턴스로 선언되어 있으면 obj 파일에 들어가나 여기서는 형만 정의한 것이다. C++빌더의 .cpp
에서는 C 함수는 stdcall 인 경우는 _ 가 붙지 않고 cdecl 인 경우는 _ 가 함수 명 앞에 붙는다. 이런 점 때문에 어떤
사람은 _ 가 없어도 된다고 하고 원저자는 경험에 의해그렇지 않다고 한 것으로 보인다.)
Henry Spence의 원래 코드에는 reggeterror(), regseterror() 함수가 없다. 이 함수를 추가한 것은 델파이
쪽에서 직접적으로 오브젝 파일에 있는 변수를 사용 할 수 없기 때문이며, 코드가 에러 값을 0으로 초기화 한 뒤, 기능 후에 에러 값을
얻으려 할 때 필요하기 때문이다. 때때로 오브젝 파일은 외부 변수를 요구하기도 한다. 만일 변수가 없다면 델파이 코드 어디엔가 존재하도록
선언해야만 한다.
대체적으로 유닛의 구현부는 아마도 다음과 같을 것이다.
implementation
uses
SysUtils;
{$LINK 'regexp.obj'}
function _regcomp(exp: PChar): PRegExp; cdecl; external;
function _regexec(prog: PRegExp; str: PChar): LongBool; cdecl;
external;
function _reggeterror: Integer; cdecl; external;
procedure _regseterror(Err: Integer); cdecl; external;
end.
|
만일 예제를 컴파일 한다면, 델파이 링커는 본래 런타임 라이브러리에 있었던 외부 참조를 해결하지 못한다고 나올 것이다. 델파이 유닛으로 이를 해결해야 한다. 대부분의 C 런타임 함수는 간단하고 델파이로 쉽게 쓰여질 수 있다. 오직 printf() 나 scanf() 같은 가변 인자 함수만 어셈블러 도움없이는 불가능할 뿐이다. 아마도 C++ 라이브러리 속에 있는 printf() 나 scanf()의 코드는 찾는다면, 오브젝 파일에서 이를 추출하고 링크도 할 수 있을 것이다. 난 해 본적이 없지만.
정규식 코드는 C 라이브러리 함수 malloc() 로 메모리 할당을 받는다. strlen() 은 문자열 길이를 계산하고,
strchr() 는 문자열에서 한 문자를 찾는다. strncmp() 는 두 문자열을 비교하고 strcspn() 은 하나의 문자열을 다른
문자열 속에서 찾아 첫번째 문자 인덱스를 돌린다.
처음 4개 함수는 간단하다. 델파이는 유사 함수가 있어 한줄이면 된다. 그러나 strcspn()은 이에 해당하는 델파이 런타임 라이브러리가 없다. 그래서 구현해야 한다. 다행히도 난 일반적으로 이 함수의 본래 C 코드를 델파이로 번역하기만 하면 되었다. 아니면 그것을 일일이 구현해야 했다.
구현한 것은 다음과 같다.
// since this unit provides the code for _malloc, it can use FreeMem to free // the PRegExp it gets. But normally, a _regfree() would be nice. function _malloc(Size: Cardinal): Pointer; cdecl; begin GetMem(Result, Size); end; function _strlen(const Str: PChar): Cardinal; cdecl; begin Result := StrLen(Str); end; function _strcspn(s1, s2: PChar): Cardinal; cdecl; var SrchS2: PChar; begin Result := 0; while S1^ <> #0 do begin SrchS2 := S2; while SrchS2^ <> #0 do begin if S1^ = SrchS2^ then Exit; Inc(SrchS2); end; Inc(S1); Inc(Result); end; end; function _strchr(const S: PChar; C: Integer): PChar; cdecl; begin Result := StrScan(S, Chr(C)); end; function _strncmp(S1, S2: PChar; MaxLen: Cardinal): Integer; cdecl; begin Result := StrLComp(S1, S2, MaxLen); end; |
보다시피 이 함수는 역시 cdecl 로 선언되어 있다.
내 프로젝트에서 이 코드를 직접 호출하여 사용하진 않는다. _RegExp 구조체는 외부에서 변경하기 힘든 비트를 사용하는
정보를 포함하고 있다. 그래서 난 RegFree 함수를 FreeMem을 호출하는 간단한 다른 함수로 래핑(재포장) 했다. _malloc()
는 GetMem을 사용한다. 바람직한 것은 정규식 코드가 regfree() 함수를 사용하는 것이다.
전체 C 소스 코드는 Import 유닛과 래퍼 유닛으로 구성된다. 내 Downloads 페이지에 있는 매우 간단한
grep 프로그램처럼.
msvcrt.dll 사용(Using msvcrt.dll)
모든 함수를 스스로 작성하는 대신, Microsoft Visual C++의 런타임 라이브러리를 사용할 수도 있다. 이는 DLL로 윈도우에서도 사용되고 있고, 모든 버전의 윈도우에 존재하고 있다.
나름대로, 이건 내 생각이 아니고 볼랜드 뉴스그룹의 Rob Kennedy로부터 제안받은 것이다. 이건 JEDI프로젝트의 일부 소스에 사용된 테크닉으로 보인다.
msvcrt.dll 사용은, 간단히 external 선언으로 위 코드를 대신한다. external 선언에 있어 DLL에는 함수에 _ 언더바를 사용하지 않는다.
// Note that you don't want to use the C memory manager, // so you must still rewrite routines like _malloc() in Delphi. function _malloc(Size: Cardinal): Pointer; cdecl; begin GetMem(Result, Size); end; // The rest can be imported from msvcrt.dll directly. function _strlen(const Str: PChar): Cardinal; cdecl; external 'msvcrt.dll' name 'strlen'; function _strcspn(s1, s2: PChar): Cardinal; cdecl; external 'msvcrt.dll' name 'strcspn'; function _strchr(const S: PChar; C: Integer): PChar; cdecl; external 'msvcrt.dll' name 'strchr'; function _strncmp(S1, S2: PChar; MaxLen: Cardinal): Integer; cdecl; external 'msvcrt.dll' name 'strncmp';
|
sprintf()나 scanf() 같은 루틴은 복잡하게 동작한다. C에서 파일 핸들을 요구하는 곳은 포인트로 선언해야 한다. 효과는 같다. 예를 들면:
function _sprintf(S: PChar; const Format: PChar): Integer; cdecl; varargs; external 'msvcrt.dll' name 'sprintf'; function _fscanf(Stream: Pointer; const Format: PChar): Integer; cdecl; varargs; external 'msvcrt.dll' name 'fscanf'; |
내 Downloads 페이지 있는 msvcrt.dll용 인터페이스 유닛 버전을 간단히 테스트 했는데, 테스트 되는대로 이 페이지를 곧 바꿀 것이다.
문제점(Problems)
그 동안 난 독자가 흥미있어 하는 몇 가지 문제에 직면했다. 이에 대해 말하자면,
난 C로 msvcrt.dll에서 Import한 수 많은 루틴을 사용하는 간단한 테스트 프로그램을 작성했다. 그러나 몇개의 루틴은 전혀 실제
루틴이 아닌 것으로 판단된다. 매크로로 구현되어 있거나 직접적으로 구조체를 다룬다던지 해서 실체로 존재하는 것은 아니나 BCB C,
델파이, MS C에서 제어 가능한 것들이다.
getchar() and putchar()
이를 테면, getchar()는 stdio.h 에 매크로로 선언되어 있는데 stdin->level 이고, stdin은 다시 &_streams[0] 매크로 이다. 만일 이 level 변수에 값이 있으면(0보다 큰 값) 버퍼에서 character를 얻고 아니면 _fgetc()를 사용한다. (델파에서는 __fgetc() 가 된다). 이런 식의 처리는 간단히 루틴을 만들어 쓰면 동일하게 된다.
이는 내가 __streams을 선언하고 level 값을 0으로 초기화함을 의미한다. 이 문제는 msvcrt.dll 루틴이 유사 구조체를 가졌고 (FILE 구조체 역시 같은 문제가 있다.), BCB 변수 _streams 으로부터 읽거나 설정할 수 없다는 데 있다. 그래서 난 델파이 버전의 __fgetc()를 작성했는데 이는 스트림 파라메터 전달이 @__streams[0]와 같은 것으로, 스트림 표준 입력 stdin이 호출 되었음을 가르킨다. 만일 그렇다면 이는 getchar() 매크로로_fgetch(stdin); 호출됨을 뜻한다. 이 경우라면 델파이의 Read를 호출할 것이고, 아니라면 msvcrt.dll에 있는 _fgetc() 루틴을 사용할 것이다.
난 이렇게 되기를 희망하지만, 그렇지 되지 못할 수 있다. 이에 관한 어떤 문제에 직면하면 내게 메일을 보내달라.
mailto:Rudy Velthuis
내 나름대로는 분석을 위해, 루틴중 하나(내 생각엔, fwrite())에 디버거로 ntdll.DbgBreakPoint에 있는 int 3 브레이크 포인트로 접근 했었다.
그런데 putchar() 루틴도 역시 매크로로 level을 다시 증가시킨다. 그래서 여기에도 유사한 문제가 있다. 난 아직 이 문제를 다루어 보진 않았다. getchar()를 만든다 해도 아마 ungetc()도 당연히 동작하지 않을 것이다. (이것 역시 매크로라서). 필요하다면 델파이에서 전체 내용을 흉내낼 수 있을 것이다. 단지 C가 몇가지 매크로를 사용하기 때문에 복잡하지만 이를 분석해서 델파이로 구현할 수는 있을 것이다.
이는 어쨌던 msvcrt.dll 로 간단히 리다이렉팅하는 경우만 있는 것은 아니므로, 쇼(기교)가 필요하다. 델파이에는 없는 C 매크로의 문제점 때문에.
내 나름대로 생각하기에는, 매크로는 악마 악마 악마다. (역자주:매크로에 고생해 본 사람은 이해될 것임.)
fgetpos() and fsetpos()
msvcrt.dll에 있는 위의 두 루틴은 pos 파라메터로 추가적인 데이타를 저장하는 것으로 보인다. BCB에서는 pos는 long int 이다. BCB의 stdio.h에 있는 fpos_t가 선언된 _fgetpos() 사용하는 것은 엑세스 바이올레이션을 야기한다. 그래서 난 루틴을 _fseek()와 _ftell()를 사용해 새로 작성했다.
이론적으로 fpos_t 는 부정형(정해지지 않은 형)이고 위 두 개의 루틴은 포인트(fpos_t *)를 사용한다. 하지만 BCB에서는
long 으로 선언되어 있는데 4바이트보다 크진 않고 어떤 값이 들어 있을 것이다. 그래서 msvcrt.dll이 long 보다 큰 값을
넣으려 한다면 몇 개 데이타는 덮어쓰기 될 것이고 당신의 프로그램은 제대로 동작하지 않거나 엑세스 바이올레이션을 야기 시킬 것이다. 이
코드를 사용하는 따른 리스크는 다른 컴파일러로 쓰여졌기 때문에 생기는 문제다.
결말 (Conclusion)
C 언어에 약간의 지식을 가지는 것이, 라이브러리 함수에 없는 C 루틴 몇개의 재작성을 두렵지 않게 하고(msvcrt.dll을 사용한다면 이 일은 매우 줄어든다) 델파이 유닛에 C 오브젝 파일을 링크하는 것이 쉽게 한다. C루틴을 작성하는 것은 DLL을 필요 없게 하나의 실행 파일로 프로그램을 만드는 것을 가능케 하기 때문이다.
커맨드라인 툴과 C 언어에 대한 정보를 뉴스 그룹을 참고하라.
news://newsgroups.borland.com/borland.public.cppbuilder.commandlinetools
The newsgroup
news://newsgroups.borland.com/borland.public.cppbuilder.language
뉴스그룹에서는 언어에 대한 질문이 가능하다.
좋은 경험의 시간이 되기를 바란다.
원저자: Rudy Velthuis