LLVM C++ 코딩 가이드라인 살펴보기 (1/3) by 김민장

LLVM은 C++ 기반의 오픈소스 컴파일러 인프라스트럭처이다. LLVM은 방대한 C++ 프로젝트이고 많은 개발자가 참여하고 있는 프로젝트이므로 적절한 코딩 가이드라인을 만드는 것은 필수이다. 좋은 내용이 많이 있기에 여기서 소개하고자 한다.

 

LLVM, Low Level Virtual Machine이란?

LLVM은 정적/동적 컴파일러와 그와 관련된 여러 도구를 만들고 조작할 수 있는 오픈소스 프로젝트이다. 일반적인 정적(static) 컴파일러는 프런트엔드(혹은 파서)가 소스코드를 해석해 중간언어(IR, Intermediate Representation)로 만든다. 이 IR은 컴파일러 백엔드(혹은 미들엔드를 두는 컴파일러도 있음)로 보내 각종 최적화를 거친 후 최종적으로 특정 아키텍처의 기계어로 코드를 생성해낸다. 동적 컴파일러 혹은 인터프리터라면 코드 생성기가 인터프리터와 간단한 가상 머신으로 대체된다. 이제는 성능 향상을 위해 순수 인터프리터를 쓰기 보다는 JIT을 한다.

LLVM은 자신만의 IR 언어를 정의하고, 이 IR를 조작하고 코드를 생성/수행하는 일을 한다. LLVM 자체는 프런트엔드를 포함하지는 않고 있다. 각 언어의 프런트엔드는 독립적으로 분리되어있다. 즉, C++ 파서가 소스코드를 분석해 LLVM IR로 만들면 비로소 LLVM 프레임웍을 쓸 수 있다.

   1: define i32 @main() {
   2: entry:
   3:   %retval = alloca i32                            ; <i32*> [#uses=2]
   4:   %0 = alloca i32                                 ; <i32*> [#uses=2]
   5:   %"alloca point" = bitcast i32 0 to i32          ; <i32> [#uses=0]
   6:   %1 = call i32 @puts(i8* getelementptr inbounds ([14 x i8]* @.str, i64 0, i64 0)) ; <i32> [#uses=0]
   7:   store i32 0, i32* %0, align 4
   8:   %2 = load i32* %0, align 4                      ; <i32> [#uses=1]
   9:   store i32 %2, i32* %retval, align 4
  10:   br label %return
  11:  
  12: return:                                           ; preds = %entry
  13:   %retval1 = load i32* %retval                    ; <i32> [#uses=1]
  14:   ret i32 %retval1
  15: }

위 코드는 “Hello, World!” 프로그램을 LLVM IR로 변환한 것이다. 보다시피 스택에 변수 하나 생성하고 함수 호출 과정이 매우 자세히 기록되어있다. 거의 기계어와 1:1로 대응이 된다. 그런데 이 LLVM IR은 특정 아키텍처에 종속적이지 않는 플랫폼 독립적이다. Low Level Virtual Machine은 바로 이런 뜻.

LLVM은 대학교에서 시작된 프로젝트였는데 LLVM을 만든 Chirs Lattner가 애플로 감으로써 LLVM은 오프소스이지만 애플이 전폭적으로 지원하는 형태가 되었다. 애플의 주요 툴체인은 이미 LLVM 기반으로 바뀌고 있다. 또, 오픈소스이다보니 여러 개발자의 참여가 매우 활발한데, 나도 처음 경험하는 직접적인 오픈소스여서 배우는 것이 많다. 지금까지 적지 않은 버그를 리포팅 하기도 하였다. (윈도우 플랫폼에서 컴파일은 제대로 되는데 제대로 작동하지 않는 부분이 많이 있음.)

LLVM은 내가 지금 하는 것처럼 프로그램을 분석하는 루틴을 작성할 때 아주 편리하다. 클래스 하나를 선언해 내가 원하는 작업을 구현하면 컴파일 단계 중 특정 부분에서 호출할 수 있다. 최근에는 동적 컴파일러 혹은 JIT도 주요한 연구 분야인데, LLVM은 역시 매우 좋은 프레임웍을 제공한다.

 

LLVM, 매우 훌륭한 C++ 기반 프로젝트의 교과서

LLVM은 C++ 언어적으로 볼 때 매우 잘 만들어진 프로젝트이다. gcc는 C로 만들어져 기존 컴포넌트를 재활용하거나 새로운 것을 추가하기가 상당히 까다로운데, LLVM에서는 약간의 학습만 거치면 놀라울 정도로 편리하게 사용할 수 있다. (물론 제대로 하려면 삽질이 엄청 따르고 버그도 직접 잡아야 하지만). 그 만큼 기초 설계가 매우 튼튼하기 때문이다. 나 역시 LLVM을 하면서 상당히 많은 C++ 공부를 새로 할 수 있었다.

LLVM에 코드를 기여하고 싶은 사람은 LLVM Coding Standard를 따라야 한다. 이미 구글 C++ 코딩 가이드라인이 유명해져 많은 개발자가 참고하고 있는데, LLVM 코딩 가이드라인 역시 흥미로운 점이 많이 있다. 서론이 너무 길었고 몇 가지를 본격적으로 살펴본다.

 

1. Source Code Formatting

이 포맷팅 관련 규칙은 가장 눈에 띄는 스타일 요소라서 프로그래머의 개성이 그대로 드러나고 바꾸는 것에 대한 저항감도 큰 것이 사실이다. 나 역시 그러했다. 나는 C/C++ 프로그래밍의 시작을 Jeffrey Richter와 Charles Petzold의 윈도우 프로그래밍과 같이해서 그들의 방식을 매우 오랫동안 따라왔다. 예를 들어, 헝그리안 표기 방식 같은 것. 그래서 다른 스타일의 코드를 보거나 쓰는 것이 불편하거나 싫어했다.

그런데 나이가 먹어서 그런가(...) 이제는 그런 저항감은 거의 없다. 한 때는 탭은 \t, 4칸이어야 한다고 주장했는데, 이제는 반드시 공백이어야 한다고 주장하고, 심지어는 2칸 공백 탭이 더욱 편리하게 되었다. LLVM은 { 를 다음 줄로 하지 않고 같은 줄에 쓰는 스타일이다. 나 역시 이 스타일은 코드를 너무 빡빡하게 만들어 상당히 싫어했는데 LLVM을 좀 만지니 어느덧 익숙해지고 말았다.

이렇듯 내 경험에 비춰볼 때, 변수 이름 규칙이나, 코드 포맷팅 규칙은 생각보다 쉽게 바꿀 수 있다고 본다. 중요한 것은 코드가 가지는 의미이지 포맷팅이 아니기 때문이다.

1.1 주석 관련

잘 짜여진 코드는 사실 따로 문서가 필요 없다. 코드 자체가 문서이기 때문이다. 그것을 가능케 하는 것이 바로 잘 작성된 주석이다. 주석 다는 것이 사실 굉장히 귀찮은 일이지만 이런 큰 오픈소스 프로젝트에서는 매우 중요하다. 실제 LLVM의 핵심 기본 클래스는 주석이 상당히 잘 되어 있어 별도의 문서를 볼 필요도 없다(없기도 하지만 ㅋ).

구체적으로 파일 헤더, 클래스 개괄, 메소드 별 정보를 주석으로 잘 적도록 지시하고 있다. 보통은 Doxygen이 바로 적용될 수 있도록 그 규칙을 따르기도 한다.

/// A Module instance is used to store all the information related to an
/// LLVM module. Modules are the top level container of all other LLVM
/// Intermediate Representation (IR) objects. Each module directly contains a
/// list of globals variables, a list of functions, a list of libraries (or
/// other modules) this module depends on, a symbol table, and various data
/// about the target's characteristics.
///
/// A module maintains a GlobalValRefMap object that is used to hold all
/// constant references to global variables in the module.  When a global
/// variable is destroyed, it should have no entries in the GlobalValueRefMap.
/// @brief The main container class for the LLVM Intermediate Representation.
class Module {
/// @name Types And Enumerations
/// @{
public:
  /// The type for the list of global variables.
  typedef iplist<GlobalVariable> GlobalListType;
  /// The type for the list of functions.
  typedef iplist<Function> FunctionListType;
  /// The type for the list of aliases.
  typedef iplist<GlobalAlias> AliasListType;
  /// The type for the list of named metadata.
  typedef ilist<NamedMDNode> NamedMDListType; 

위 코드는 Module.h 파일인데 보다시피 자세한 설명과 Doxygen의 규칙을 따르고 있다. 아래 코드는 Module.cpp 코드의 메소드 구현에 있는 주석이다.

//===----------------------------------------------------------------------===//
// Methods for easy access to the functions in the module.
//
 
// getOrInsertFunction - Look up the specified function in the module symbol
// table.  If it does not exist, add a prototype for the function and return
// it.  This is nice because it allows most passes to get away with not handling
// the symbol table directly for this common task.
//
Constant *Module::getOrInsertFunction(StringRef Name,
                                      const FunctionType *Ty,
                                      AttrListPtr AttributeList) {
  // See if we have a definition for the specified function already.
  GlobalValue *F = getNamedValue(Name);
  if (F == 0) {

한편, LLVM은 대량의 코드를 주석 처리할 때, #if 0, #endif 조합을 쓸 것을 권장한다. 적극 동감하는 바이다.

1.2 #include 스타일

//===-- Module.cpp - Implement the Module class ---------------------------===//
//
//                     The LLVM Compiler Infrastructure
//
// This file is distributed under the University of Illinois Open Source
// License. See LICENSE.TXT for details.
//
//===----------------------------------------------------------------------===//
//
// This file implements the Module class for the VMCore library.
//
//===----------------------------------------------------------------------===//
 
#include "llvm/Module.h"
#include "llvm/InstrTypes.h"
#include "llvm/Constants.h"
#include "llvm/DerivedTypes.h"
#include "llvm/GVMaterializer.h"
#include "llvm/LLVMContext.h"
#include "llvm/ADT/SmallString.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/ADT/StringExtras.h"
#include "llvm/Support/LeakDetector.h"
#include "SymbolTableListTraitsImpl.h"
#include "llvm/TypeSymbolTable.h"
#include <algorithm>
#include <cstdarg>
#include <cstdlib>
using namespace llvm;

C/C++의 가장 지저분한 것을 고르라면 단연코 #include 시스템일 것이다. 아무 생각 없이 #include를 남발하면 복잡한 의존성이 생겨버려 코드 한 줄 고치면 전체 파일이 다 컴파일되는 비극도 일어날 수 있다. 특히 큰 프로젝트일수록 깔끔한 #include 관리는 매우 중요하다. LLVM 역시 구체적인 규칙을 명시하고 있다.

위 코드 예에서 보듯, #include는 헤더 주석 뒤에 바로 나와야 한다. 첫 번째 #include는 반드시 해당 파일의 헤더가 와야 한다. 이 예에서는 Module.cpp이므로 Module.h가 보다시피 첫 번째로 온다. 이렇게 하는 이유는 Module.h가 처음에 나옴으로써 Module.h 자체가 필요한 #include는 반드시 Module.h에 있도록 강제할 수 있다. 그러하지 않다면 분명 정의를 못 찾아 컴파일 에러가 뜰 것이기 때문이다.

이렇게 자신의 헤더 파일을 #include 한 뒤에 이 파일만이 사용하는 헤더가 오고, LLVM의 공통 헤더가 알파벳 순서대로 나열된다. 마지막으로 일반적인 C/C++ 헤더가 온다.

LLVM 코딩 가이드라인에서는 구체적으로는 말을 안 하고 있는데, 큰 프로젝트에서는 폴더가 필연적으로 많아지니 헤더 파일의 상대경로 역시 적절히 적어주는 것이 중요하다. 위의 예에서는 llvm을 기준점으로 라이브러리별로 #include를 하는 것을 볼 수 있다. 그런데 “SymbolTableListTraitsImpl.h” 처럼 상대 경로 없이 바로 있는 것도 있다. 나는 이걸 좋아하지는 않는다. 묵시적으로 그냥 지금 이 Module.cpp와 같은 폴더에 있다는 뜻이겠지만, 컴파일러 옵션에 별도의 참조할 include 폴더를 정의한다면 다른 폴더가 될 수도 있다. 그래서 예전 회사에서 작업하던 큰 프로젝트에서는 ‘모든’ 헤더 파일에 반드시 상대 경로를 명시하도록 했다. 그리고 컴파일러 옵션에 최상위 폴더 외에 다른 추가 참조 include 폴더는 쓰지 않도록 했다.

1.2 소스 폭, 탭

나처럼 vim/emacs를 싫어하는 사람으로서는 소스 폭이 중요하다고 생각해 본적이 별로 없었다. 그러나 터미널에서 작업하는 것도 중요한 vim/emacs 유저에겐, 또 프린팅을 고려한다면, 소스 폭을 제한하는 것이 합리적이다. 구글에서도 LLVM에서도 모두 80 컬럼으로 제한하고 있다. 모든 코드가 칼 같이 80 컬럼에 맞춰있다. 역시 쉬운 논쟁의 대상인지라 LLVM/구글 모두 80 컬럼을 명확하게 말하고 있다. 또 하나의 이점은 80 컬럼 정도로 제한을 두면 와이드 모니터에서 두 소스를 동시에 놓고 작업할 때 상당히 편리해진다. 처음에는 좀 받아들이기 힘들었는데 강제적인 80 컬럼 제한이 나름 의미가 있다고 생각한다. (Visual Studio 2010에는 익스텐션을 활용하여 80 컬럼에 가이드라인을 그을 수 있다.)

탭 역시 \t이냐 공백이냐 논쟁이 있는데, LLVM (구글 역시) 공백을 사용토록 하고 있다. 다만, 구글은 두 칸의 공백을 쓰도록 했다면, LLVM은 두 칸인지 네 칸인지 명시하지는 않고 있다. 이건 자유에 맡기지만, 결코 2칸, 4칸 탭을 섞어서는 안 된다. 그러나 이미 대부분의 핵심 LLVM 코드는 두 칸 공백을 사용하고 있다. 80 컬럼 제한에서 탭을 4칸으로 하면 너무 쓸 수 있는 코드가 없으므로 사실상 두 칸 공백은 필수적이다.

코드를 어떻게 인덴트할 것인가 역시 큰 논쟁거리다. 또 연산자 사이에 공백을 어떻게 넣을 것인가도. LLVM은 명확하게 규정하고 있지는 않고 잘 알아서 하도록 당부하고 있다.

 

열 외: 포인터 표기법

이건 LLVM에는 없는 내용이지만 포인터 표기 법에 대한 것이다.

   1: Value   *DoSomething(Instruction *I);
   2: Value*   DoSomething(Instruction* I);

역시 논쟁이 많다. 나는 후자를 강력하게 지지한다. LLVM은 대부분 1번 방식을 따른다. 내가 2번 방식을 좋아하는 이유는 ‘Value*’가 하나의 ‘타입’이기 때문이다. 마찬가지로 함수 인자로 넘어가는 I도 그 타입이 ‘Instruction*’라는 것이다. typedef로 포인터 타입을 다른 이름으로 대체한다면 이는 더욱 합리적이라고 생각한다. 1번 방식이 자연스러운 경우는 *가 타입 선언이 아니라 값을 읽어오는 연산자로서 사용될 때이다. 즉, ‘*I’는 포인터 변수 I의 값을 읽는다는 뜻이 더욱 있다고 본다. 그래서 나는 1번 표현 방식이 늘 거북하다.

굳이 1번 방식이 좋은 경우라고는:

   1: int *a, *b, *c;
   2: int* a, *b, *c;

이렇게 포인터 변수를 한 줄에 선언할 때인데, 나는 변수 선언은 가급적 한 줄에 하나씩 하는 것을 좋아하기에 큰 문제가 되지 않는다. 암튼 이 포인터 표기 방법은 논란 거리인데, 역시나.. 그냥 이미 있는 코드가 다른 방식을 쓴다면 그렇게 따라 하는 것이 속 편하다. 어렸을 때는 이런 것도 흥분하며 논쟁했는데, 이제는 그냥 아무것도 다 좋아요. 중요한 건 버그 없고 의미만 명확하면 되니깐.

 

쓰다 보니 글이 너무 대책 없이 길어져서 다음 글에서 본격적인 C++ 관련 내용을 쓰도록 하겠습니다. 여러분의 다양한 의견 역시 듣고 싶습니다.


덧글

  • uriel 2011/05/04 18:24 # 답글

    최근에 가장 관심이 많이 가는 부분이 LLVM 쪽이긴 한데, 해당 부분을 다루어 주셔서 감사합니다. 혹시 reference로 볼만한 것을 추천해주실 수 있을까요?
  • 김민장 2011/05/04 18:27 #

    딱히 레퍼런스라는 것은 없고 그냥 LLVM을 다운 받고 해보면 됩니다. 글에서도 적었듯이 LLVM은 매우 광범위한 프로젝트입니다. 프런트엔드만 빼고 다 있어서 사용 목적에 따라 봐야하는 부분도 천차만별입니다. 저 같이 코드분석/최적화를 하는 사람은 LLVM의 pass를 만들어서 하면 되고요. Code generation하는 사람은 또 다른 부분을 봐야하고요. JIT/다이나믹 컴필레이션 하는 사람은 또 다른 부분을 봐야하고. 스태틱 컴파일러를 만들고 싶은 사람 역시 다른 부분을 봐야하고. LLVM 소스를 받으면 예제가 많이 있습니다. 원하시는 목적에 따라 하나 보면서 시작하는 것이 가장 좋습니다. LLVM 자체가 잘 짜여있어서 특별히 문서를 많이 볼 필요는 없더라고요.
  • 김민장 2011/05/04 18:30 #

    소스가 상당히 크다 보니 역시 소스 브라우징이 핵심. vim 잘 쓰는 친구도 소스 브라우징을 헤메더라고요. API 찾는 것은 문서보다 직접 헤더 파일 뒤지는게 빠를 정도. 저도 좀 빡시게 만져봤는데 버그도 많고 그렇지만 이 정도면 상당히 훌륭합니다. 매일매일 수많은 파일이 변경되고 커밋되고 빠르게 진화하고 있음.
  • summerlight 2011/05/04 18:43 # 삭제 답글

    개인적으로 탭/공백은 목적(들여쓰기/포맷팅)에 따라 구분해서 쓰고 있는데, 포맷팅에 탭을 쓰는 경우만 없으면 어떤 방향이라도 큰 문제는 없다고 보고 있습니다. 요점은 탭 사이즈가 얼마건 모양새가 깨지지 않도록 하는 것이니까요.

    스마트 포인터 등의 활용을 생각해본다면 포인터 변수 선언은 전자가 맞다고 생각합니다만, 사실 앞이냐 뒤냐 가지고 싸우기보다는 아예 여러 변수를 한 줄에 선언하지 않게 강제하고 위치는 알아서 쓰도록 하는 게 좀 덜 소모적인 느낌입니다. 보통 이 쪽이 실수도 덜 나오고요.
  • 김민장 2011/05/06 10:01 #

    포맷팅은 저도 많이 유연해져서 뭐든 괜찮다는 입장입니다. 그런데 포인터는 여전히..

    void *Food();
    void Good();

    이런 경우도 왕왕 있어서 *를 타입에 붙여쓰는 것을 훨씬 선호는 합니다 ㅎㅎ
  • 김민장 2011/05/06 10:02 #

    공백이 날라가는데... void와 * 사이에 아주 큰 공백이 있는 경우입니다. 보통 수직 방향으로도 예쁘게 줄을 맞추다보면 void와 *Food 사이에 큰 공백이 있을 수도 있죠. 그러면 좀 불편하죠...
  • summerlight 2011/05/06 11:19 # 삭제

    아 저도 함수 선언에다가 저렇게 asterisk 붙여 놓은 거 보면 막 코드를 뜯어 고치고 싶어서 손이 부들부들 떨린다능 ... ㅜㅜ
  • 미친병아리 2011/05/04 23:06 # 답글

    이야.. 재밌겠는데요.. 주말에 구경함 해봐야겠습니당..
  • 도달 2011/05/04 23:26 # 답글

    포인터 표기법은 저랑 비슷한 생각을 가지고 계시네요. 저도 한 줄에 변수 한개만 선언하는 편이다보니 저렇게 하는 것이 여러가지 면에서 좋더라구요.
  • 김민장 2011/05/06 10:02 #

    그런데 일반적으로 void *foo 가 훨씬 많이 쓰이는 것 같습니다;;
  • felucca 2011/05/05 03:29 # 삭제 답글

    if 0 endif 의 좋은 점이 뭔가요?
  • 김민장 2011/05/06 03:04 #

    적은 비용으로 껏다 켰다할 수 있다는 점............
  • uriel 2011/05/05 18:50 # 답글

    하나 추가적인 질문이 있는데, LLVM IR의 경우 어느 정도 stable한지요? 극단적으로 LLVM assembler 로만 코딩을 해도 당분간 spec 변경으로 안돌아가지는 않을 정도인가요? (이럴 지는 잘 모르겠지만 ^^)
  • 김민장 2011/05/06 03:05 #

    이미 LLVM은 애플의 툴체인으로서 사용되고 있습니다. 그러니 그런 문제는 절대 없습니다. 언어에 있어 하위호환성은 생명이니깐요. 그러나 당연히 새로운 명령어는 추가될 수 있습니다. 새로운 SIMD나 벡터 명령어나 그런 건 들어갈 수 있겠지만 지금의 기본적인 IR이 바뀔리는 없습니다.
  • 리피 2011/05/06 08:29 # 삭제 답글

    include디렉토리를 컴파일시 지정하는 것과 소스에 직접 명기하는 것과의 기능적인 차이가 있나요? 특별히 소스에 명기하는 걸 선호하는 이유에 대한 언급이 없어서 여쭤봅니다.
  • 김민장 2011/05/06 10:00 #

    #include "asdf.h"
    #include "bcef.h"

    이렇게 했을 때, 일반적으로는 asdf와 bcde라 같은 폴더에 있지만, 컴파일러 옵션으로 include 디렉토를 여러 개 명기했다면 저 두 파일이 다른 곳에 있을 수 있고 이는 혼란을 줄 수 있습니다. 실제로 이런 경우가 많습니다. 헤더 파일이 몇 개 안 되면 모를까 수백 수천 개가 있다면 이야기는 달라집니다. 항상 동일한 루트로부터 상대 경로를 모두 적어두는 것이 훨씬 편리합니다. 물론 간단한 프로젝트에서도 그렇게 하라는 소리는 아닙니다.
  • 일장춘몽 2011/05/06 13:40 # 삭제 답글

    안그래도 눈여겨 보고 있는 프로그램입니다. 나중에 나만의 언어를 만들어 볼 때 백엔드로 쓰면 좋을 것 같아서요. 어떤 용도로 활용하고 계신지요?
  • 김민장 2011/05/08 01:01 #

    저는 프로그램 분석과 프로파일링에 사용하고 있습니다.
  • yekki 2011/05/08 21:53 # 삭제 답글

    포인터표기법등들은 이미 많은 시간이 지났고 또 그만큼 호불호가 확실히 갈려있는만큼 통일된 표기법을 기대하는게 힘들지 않을까 싶네요. 이렇게 쓰나 저렇게 쓰나 다르진 않으니까요. 결국 그 화살은 자신한테 돌아오지만요-_-ㅋ 하드웨어랑 친하지 않지만 많이 배우고 또 항상 많이 보고 생각하고 느끼고 있습니다 (__)
  • 김민장 2011/05/12 10:54 #

    그래서 파이썬처럼 아예 인덴트를 강제하는 것도 좋다고 생각.....
  • LimeBurst 2011/05/17 21:13 # 삭제

    그럼 IOCCC는 어떻게 되나요ㅠㅠ
  • zelon 2011/05/10 01:48 # 삭제 답글

    오랜만에 매우 흥미로운 글 보고 갑니다. 저도 이래저래 남의 코드들을 보면서 이제 포맷팅은 현재 프로젝트에 맞게 쓰자라는 주의지만... { 를 앞의 줄 끝에 쓰는게 제일 거슬리네요;; 다른 건 뭐 이런들 어떠하리 저런들 어떠하리 ^^;

    include 경로는... 제가 하고 있는 곳에서는 컴파일러에 경로를 바꿔서 모듈을 바꿔버리는게 좀 있어서, 항상 루트 디렉토리를 적어주기는 힘들거 같더라구요. 자유도가 높으면, 혼란이 있을 수 있는건 어쩔 수 없는 장/단점인 듯 싶습니다.
  • 김민장 2011/05/12 10:54 #

    말씀대로 include 경로는 프로젝트 성격마다 다릅니다.
  • limited10 2011/05/12 09:05 # 삭제 답글

    JIT도 신기한데 LLVM은 로우레벨임에도 VM에서 실행이 된다니 굉장히 신기하네요!
    이클립스를 쓰다가 Editor 메뉴에 한 줄 코드 길이 제한을 봤는데 그게 이런 의미군요 ㅎㅎ
    오픈소스 정말 재밌고 배울게넘치네요!!
  • 김민장 2011/05/12 10:53 #

    LLVM이 말하는 VM이 VMware 수준의 VM은 아닙니다. 그 보다는 훨씬 간단한 플랫폼 독립적인 명령어를 가져다 바로 실행(인터프리팅/JIT)해주는 것을 그냥 통칭 VM이라 하는 것이죠. JVM이랑 같은 개념으로 보면 됩니다.
  • 염원영 2011/05/12 14:35 # 삭제 답글

    저도 포인터 변수 표기할 때 두번째 방법을 선호합니다. 하지만 일부 번역서를 보면 첫번째로 사용하기를 권장하더군요...

    처음 C/C++ 배울 때는 괄호 열고 다음줄 부터 바로 시작(비대칭형) 으로 시작했는데
    요즘에는 그냥 대칭형으로 사용하고 있습니다. C# 이나 ActionScript 에서는 그렇게 하는 것이 표준으로 되어 있더군요.

    그리고 include 문같은 IDE 나 서드파티 툴에서 자동으로 정렬하고 없는 파일은 제거해주는 것이 좋은 것 같습니다. ActionScript 같은 경우에 import 문을 정렬하거나 사용하지 않는 또는 없는 것은 자동으로 제거해 주는 기능이 이클립스에 포함되어 있습니다.
    Visual C++ 의 경우에는 Visual Assist 같은 툴의 도움을 받으면 깔끔하게 처리될 것 같습니다.
  • 달리나음 2011/05/27 16:39 # 삭제 답글

    cdecl과 같은 도구를 쓰시거나 the c programming language로 시작한 사람이라면 identifier를 주위로 좌우로 먼저 읽는 습관이 있으실 겁니다. (사실 문법 자체가 identifier에서 가까운 곳 부터 읽도록 설계되었습니다.) 그런 경우 int* a; 식으로 identifier에서 멀어진다면, 오히려 읽기가 어렵죠.

    그리고 80라인에서 tab을 적는 것에서도 여러 컨벤션이 있을텐데요. 커널 쪽은 8칸 indent에 tab 문자를 쓰는게 일반적이죠. Linus와 같은 사람들은 하나의 함수에서 여러 depth가 쓰이는 것을 선호하지 않기도 합니다.

    저는 명칭은 역시 identifier에서 가깝게 적어야 하고요. 인덴트는 C언어에서는 8칸 C++/JAVA/Perl에서는 4칸. 그리고 탭문자는 무조건 8칸인 것을 선호하는 편입니다.
  • 달리나음 2011/05/27 16:56 # 삭제

    하지만 오픈소스 프로젝트들의 다양한 컨벤션들에 대해서는 맞춰서 쓰는 편입니다. 해당 프로젝트에서 해당 컨벤션을 쓰게 된 문화적인(?) 이유들이 타당할 경우가 많더군요. 그 과정에서 얻는 무엇도 있는 것 같습니다.
  • 김민장 2011/05/27 17:23 #

    Formal하게 포인터/배열/함수 선언을 읽는다면 말씀처럼 *가 변수명 옆에 오는 것이 합리적입니다. int (*a())() 뭐 이런 걸 쓸 때는 *를 띄어 쓰는 것이 훨씬 어색합니다. 말씀처럼 왔다갔다 읽을 때.. a is a function returning a poin... 생략;

    그런데, 이런 선언문 자주 사용하나요? 끽해봐야 콜백함수 선언 같은 것인데 그것도 저는 항상 typedef를 씁니다. C/C++ 언어로 high-order 짓거리를 자주 하면 모를까 ㅎㅎ C++0x의 auto 덕에 그나마도.

    대부분의 포인터는 그래서 int*, complex** 같은 객체/primitive 타입을 가리키는 용도로 쓰일 겁니다. 그렇다면 오히려 제가 주장한 것처럼 int*가 더 편하게 느껴질 수 있습니다. 복잡하게 생각할 필요 없이 int*를 하나의 타입으로 바로 볼 수 있는 것이죠. int**가 되더라도, int*를 가리키는 포인터 타입, 으로 읽으면 간단하고요. Win32 API에서는 *타입을 typedef로 하나의 변수명으로 바꾸기도 합니다. char* a; 를 PCHAR a;로 하기도 하죠. (별로 이게 좋다고 생각은 안 합니다.)

    그래서 "int *a;"는 저에게 어떤 느낌이냐면 "in t a;" 뭐 이런 느낌이랍니다.

    그러나 위에서 말했듯이... 기존의 코드가 int *a;를 썼다면 그냥 따라 씁니다. 논쟁은 어렸을 때나 하고 이제는 그냥저냥 뭐 다 좋다는.


    여담이지만 저는 리누스 토발즈를 안 좋아합니다. 리얼월드텍의 스레드를 탐독하는데 토발즈처럼 싸가지 없게 확신에 차 말하는 사람을 본 적이 없습니다;; 일단 권위가 있다보니 굉장히 고압적으로 들리더라고요. 그 사람 댓글 보면서 겸손해야지... 라고 늘 반성할 정도랍니다. 역시 리눅스 커널 코딩 컨벤션을 봐도 8t가 왜 좋지에 대한 주장 역시 상당히 자의적이고 게다가 너무나 확신에 차 있습니다.

    http://www.kernel.org/doc/Documentation/CodingStyle

    물론 토발즈의 주장, 뎁스가 깊어지면 골치 아픕니다. 그래서 최대한 early return을 해버리고 인덴트를 줄이려고 합니다. 탭 2칸은 사실 for 3개 정도 중첩이 되어있으면 헷갈리는 거 맞습니다 ㅎㅎ

    뭐 저는 리눅스 커널을 구경할 일이 없는 사람이라.. 그냥 그렇게 살고 있습니다. 다시 말하지만 이래도 좋고 저래도 좋고... 버그 없고 의미만 명확하면 만사 okay.
  • 달리나음 2011/05/28 02:43 # 삭제

    여러 계층으로 되어 있는 C언어 프로그래밍이라면 복잡한 형태의 포인터는 피할 수 없는 것이 아닌가 생각합니다. 구조체와 복잡한 형태의 포인터를 클라이언트가 서버로 등록을 하고, 서버 혹은 그 서버의 상위 계층이 그 구조체를 포인터를 사용해야 하니깐요. 함수 포인터를 받는 함수 포인터와 포인터의 포인터들도 흔하게 쓰이죠. 커널, 윈도우 시스템을 만들 때는 이런 표현들을 많이 쓰지 않는다면 어떻게 코딩해야할지 까마득할 것 같습니다. 저는 이런 상황에서 typedef를 별로 좋아하지 않는 편인데 제가 외울 수 있는 typedef의 수는 한정적이고 문제가 발생할 때 typedef를 뒤지는 것보다 투명한 구현이 더 맘에 듭니다.

    생각해보니 C++에서는 객체 시스템을 통해 해결할 수 있는 부분도 많을테고 그러한 환경에서는 프리미티브나 객체 자체에 대한 reference를 주로 사용하게 될 수도 있겠습니다. 아마도 그런 이유가 많은 GUI 시스템 자체가 C++로 작성된 이유이기도 하겠지요. GNOME은 아예 C언어에 객체 모델을 만들어버렸고요. 어떻게 보면 커널 자체도 지금은 C++과 같은 언어로 짜는게 더 합리적일지도 모르겠네요.

    (말하고 보니 C언어까, C++언어빠처럼 보이는 군요 (...) 뭐 저는 둘다 두 언어 다 애증이 있고요 C++은 템플릿을 이용하는 정책 기반의 템플릿 메타 프로그래밍 측면에서 좋게 생각하는 사람이기도 합니다. 어쩌다 보니 C언어를 많이 써왔고 저런 코딩도 해왔고 앞으로도 계속 그렇게 할 것 같습니다... )

    여담으로 저는 int *a;이든 int* a;이든 지금의 형태가 맘에 들지 않습니다. 저는 아래와 같은 문법이었으면 합니다. 일관성있어 우선순위가 필요없고, 어순과도 일치하는, 더 합리적인 문법이라 생각합니다.

    int a*;
    int a*[5];
    int a[5]*;
    int argv*[];
    int argv[]*;
    int func*();
    int func()*;

    type에 가까이 있어야 한다면 아래와 같이 바꿀 수 있겠죠.

    a *int;
    a *[5]int;
    a [5]*int;
    argv *[]int;
    argv []*int;
    func *() int;
    func ()* int;

    어떻게 하면 문법적으로 일관성있으면서도 더 가독성이 좋을지 감이 안잡히는 군요. Java 어레이 문법 형태는 결정을 회피한 것 같아 맘에 들지 않고요.
  • 김민장 2011/06/11 12:55 #

    예전 블로그에서 주장하신 표현 방법을 봤습니다. 표현 방식에는 별 생각이 없었는데 덕분에 이런 저런 생각을 해보았죠. 저는 여전히 타입을 나타내는 건 한 쪽으로 몰아쓰는 것이 훨씬 직관적이라고 생각합니다. 말씀하신 어순은 아쉽게도 기계적인 어순이지 사람의 어순은 아닙니다. 함수 포인터만 제외하면 다른 변수 선언을 cdecl 방식으로 선언을 읽어야 할 경우도 거의 없고요. C#에도 delegate, event로 함수 포인터를 훨씬 쉽게 기술할 수 있고 C++0x의std::function 도입으로 역시 함수 포인터 표기법을 개선하려 하고 있습니다.

    typedef에 대해서는 IDE/인텔리센스의 도움이 있다면 외워야할 압박이 전혀 필요 없습니다. 함수포인터를 typedef로 거의 무조건 제가 표기하는 이유는 일종의 추상화입니다.
  • 달리나음 2011/06/28 10:42 # 삭제

    저도 더 추상화된 도구들이 도움이 된다고 생각합니다. 하지만 typedef의 쓰임이 유리한지는 저는 의문이 듭니다.

    typdef에 pointer to가 위치하는 경우 부분적으로 떼어내는것이 불가능해지는 특성이 있습니다.

    typedef struct asdf* aaa_h;
    void function(const aaa_h handle, int xxx)

    보통은 pointer to constant struct asdf라고 생각하는데 typedef안에서만 constant pointer to struct asdf가 됩니다. typedef를 단순히 치환이라고 생각한다면 이 부분에서 오류를 범할 수 있죠. 개념적으로 직관적이지 않기도 하고요.

    C프로그래밍은 시스템이 커지면 복잡한 표현을 쓸 수 밖에 없는데 typedef는 프로그래머를 혼란스럽게 하지 않나 생각합니다.
  • erinjslee 2011/11/05 15:58 # 삭제 답글

    LLVM을 소개하는 글을 간단히 블로그에 기록했는데, IR생성하는데 사용한 c code을 남기게 되었는데 LLVM의 소스 코드 포맷에 대해서 간단하게 언급하면서 이 글의 링크를 남겼습니다. 괜찬을까요?

    안되겠다고 말씀하신다면 바로 지우도록 하겠습니다.
  • 김민장 2011/11/05 18:24 #

    아무런 문제 없습니다. 참고로 이렇게 글을 공개적인 블로그에 올렸다는 것은 자유로운 인용을 허용한다는 뜻이니까 허락 받지 않으셔도 됩니다.
  • erinjslee 2011/11/06 00:27 # 삭제

    감사합니다....
    요즘 글이 공개적으로 올라왔다고 해서 링크를 꼭 할 수 있는 건 아닌 듯한 분위기라 조심스럽습니다,

    어쨌든 허락해 주셔서 감사합니다.

    그러고보니 어디에다 링크를 남겼다고 링크조차 남기지 않았네요.....
    http://eriny.net/n/67
  • 123 2012/08/19 00:01 # 삭제 답글

    포인터 왼쪽에 쓰는사람, 가운데 쓰는사람, 오른쪽에 쓰는사람 가지각색인데요.
    보통 C 배우는 사람들이 ANSI C 로 시작하지만 C 언어 가 K&R 에 의해 만들어져서 사용됬던 언어라
    K&R 사용법을 많이 따르지 않나 생각되는데요
    C Programming Tutorial (K&R version 4)
    http://www.iu.hio.no/~mark/CTutorial/CTutorial.html
    여기 한번 훌터 보시면 왜 포인터를 펑션네임에 붙여서 사용하는지 좀 이해가 되실거라 생각합니다.
    가령 소스를 보시면 따로 forward declaration 이없고
    함수네에 변수와 같이 사용되는 함수를 선언해서 사용하더라고요
    char ch, *getname(), getchar(); 이런식으로요
    그래서 제가 내린결론은 identifier 가 있을경우 변수이름이건 함수이름이건 어레이건 간에 identifier 에 붙여서 사용하고 따로 identifier 가 없는 경우는 타입에 붙여서 사용
  • 미소 2015/10/03 10:37 # 삭제 답글

    리버스 엔지니어링 관련해서 친구가 LLVM 을 공부해보라고 해서 보고 있는 학생입니다.

    제가 부족해서 잘 이해가 안가는데.. 컴파일을 빠르고 보기 쉽게 해주는 LLVM과 리버싱이 무슨 관계가 있는건가요?

    프로그램 분석에 사용하신다고 하셨는데 어떤 식으로 적용시키신건지 궁금합니다.

    답변해주시면 감사하겠습니다. ^^
  • 김민장 2015/10/03 17:31 #

    리버스 엔지니어링은 저도 잘 아는 분야가 아닙니다. LLVM은 쉽게 설명하면 C++로 비교적 사용하기 쉬운 컴파일러(프런트엔드부터 백엔드까지) 오픈소스입니다. 프로그램 분석에 용이한 점은 소스코드 파싱 이후 과정을 볼 수 있으므로 여기에 자신만의 분석 알고리즘을 적용할 수 있습니다. 예를 들어 자신만의 최적화 루틴 등을 쉽게 작성할 수 있습니다.
댓글 입력 영역