X-Macros의 실제 사용
방금 X-Macros에 대해 배웠습니다 . X-Macros의 실제 용도는 무엇입니까? 작업에 적합한 도구는 언제입니까?
몇 년 전에 코드에서 함수 포인터를 사용하기 시작했을 때 X- 매크로를 발견했습니다. 저는 임베디드 프로그래머이고 상태 머신을 자주 사용합니다. 종종 다음과 같은 코드를 작성합니다.
/* declare an enumeration of state codes */
enum{ STATE0, STATE1, STATE2, ... , STATEX, NUM_STATES};
/* declare a table of function pointers */
p_func_t jumptable[NUM_STATES] = {func0, func1, func2, ... , funcX};
문제는 내 함수 포인터 테이블의 순서를 유지하여 상태 열거 순서와 일치하도록 유지해야하는 오류가 발생하기 쉽다고 생각했다는 것입니다.
내 친구가 X- 매크로를 소개했는데 마치 전구가 내 머릿속에서 꺼지는 것 같았습니다. 진지하게, 당신은 내 평생 X- 매크로 어디 있었습니까!
이제 다음 테이블을 정의합니다.
#define STATE_TABLE \
ENTRY(STATE0, func0) \
ENTRY(STATE1, func1) \
ENTRY(STATE2, func2) \
...
ENTRY(STATEX, funcX) \
그리고 다음과 같이 사용할 수 있습니다.
enum
{
#define ENTRY(a,b) a,
STATE_TABLE
#undef ENTRY
NUM_STATES
};
과
p_func_t jumptable[NUM_STATES] =
{
#define ENTRY(a,b) b,
STATE_TABLE
#undef ENTRY
};
보너스로 전처리 기가 다음과 같이 함수 프로토 타입을 빌드하도록 할 수도 있습니다.
#define ENTRY(a,b) static void b(void);
STATE_TABLE
#undef ENTRY
또 다른 사용법은 레지스터를 선언하고 초기화하는 것입니다.
#define IO_ADDRESS_OFFSET (0x8000)
#define REGISTER_TABLE\
ENTRY(reg0, IO_ADDRESS_OFFSET + 0, 0x11)\
ENTRY(reg1, IO_ADDRESS_OFFSET + 1, 0x55)\
ENTRY(reg2, IO_ADDRESS_OFFSET + 2, 0x1b)\
...
ENTRY(regX, IO_ADDRESS_OFFSET + X, 0x33)\
/* declare the registers (where _at_ is a compiler specific directive) */
#define ENTRY(a, b, c) volatile uint8_t a _at_ b:
REGISTER_TABLE
#undef ENTRY
/* initialize registers */
#define ENTRY(a, b, c) a = c;
REGISTER_TABLE
#undef ENTRY
그러나 내가 가장 좋아하는 사용법은 통신 핸들러와 관련하여
먼저 각 명령 이름과 코드를 포함하는 comms 테이블을 만듭니다.
#define COMMAND_TABLE \
ENTRY(RESERVED, reserved, 0x00) \
ENTRY(COMMAND1, command1, 0x01) \
ENTRY(COMMAND2, command2, 0x02) \
...
ENTRY(COMMANDX, commandX, 0x0X) \
대문자는 열거 형에 사용되고 소문자는 함수 이름에 사용되기 때문에 테이블에 대문자와 소문자 이름이 모두 있습니다.
그런 다음 각 명령에 대한 구조체를 정의하여 각 명령의 모양을 정의합니다.
typedef struct {...}command1_cmd_t;
typedef struct {...}command2_cmd_t;
etc.
마찬가지로 각 명령 응답에 대한 구조체를 정의합니다.
typedef struct {...}command1_resp_t;
typedef struct {...}command2_resp_t;
etc.
그런 다음 명령 코드 열거를 정의 할 수 있습니다.
enum
{
#define ENTRY(a,b,c) a##_CMD = c,
COMMAND_TABLE
#undef ENTRY
};
명령 길이 열거를 정의 할 수 있습니다.
enum
{
#define ENTRY(a,b,c) a##_CMD_LENGTH = sizeof(b##_cmd_t);
COMMAND_TABLE
#undef ENTRY
};
내 응답 길이 열거를 정의 할 수 있습니다.
enum
{
#define ENTRY(a,b,c) a##_RESP_LENGTH = sizeof(b##_resp_t);
COMMAND_TABLE
#undef ENTRY
};
다음과 같이 몇 개의 명령이 있는지 확인할 수 있습니다.
typedef struct
{
#define ENTRY(a,b,c) uint8_t b;
COMMAND_TABLE
#undef ENTRY
} offset_struct_t;
#define NUMBER_OF_COMMANDS sizeof(offset_struct_t)
참고 : 실제로 offset_struct_t를 인스턴스화하지 않습니다. 컴파일러가 명령 정의 수를 생성하는 방법으로 사용합니다.
다음과 같이 함수 포인터 테이블을 생성 할 수 있습니다.
p_func_t jump_table[NUMBER_OF_COMMANDS] =
{
#define ENTRY(a,b,c) process_##b,
COMMAND_TABLE
#undef ENTRY
}
그리고 내 함수 프로토 타입 :
#define ENTRY(a,b,c) void process_##b(void);
COMMAND_TABLE
#undef ENTRY
이제 마지막으로 가장 멋진 용도로 컴파일러가 전송 버퍼의 크기를 계산하도록 할 수 있습니다.
/* reminder the sizeof a union is the size of its largest member */
typedef union
{
#define ENTRY(a,b,c) uint8_t b##_buf[sizeof(b##_cmd_t)];
COMMAND_TABLE
#undef ENTRY
}tx_buf_t
다시이 유니온은 내 오프셋 구조체와 같으며 인스턴스화되지 않고 대신 sizeof 연산자를 사용하여 전송 버퍼 크기를 선언 할 수 있습니다.
uint8_t tx_buf[sizeof(tx_buf_t)];
이제 전송 버퍼 tx_buf가 최적의 크기이며이 통신 핸들러에 명령을 추가하면 버퍼가 항상 최적의 크기가됩니다. 멋있는!
한 가지 다른 용도는 오프셋 테이블을 만드는 것입니다. 메모리는 종종 임베디드 시스템에서 제약이되기 때문에 희소 배열 일 때 점프 테이블 (포인터 당 2 바이트 X 256 개의 가능한 명령)에 512 바이트를 사용하고 싶지 않습니다. 대신 가능한 각 명령에 대해 8 비트 오프셋 테이블이 있습니다. 이 오프셋은 이제 NUM_COMMANDS * sizeof (pointer)이면되는 실제 점프 테이블로 인덱싱하는 데 사용됩니다. 제 경우에는 10 개의 명령이 정의되어 있습니다. 내 점프 테이블은 길이가 20 바이트이고 길이가 256 바이트 인 오프셋 테이블이 있는데 이는 512 바이트 대신 총 276 바이트입니다. 그런 다음 내 함수를 다음과 같이 호출합니다.
jump_table[offset_table[command]]();
대신에
jump_table[command]();
다음과 같이 오프셋 테이블을 만들 수 있습니다.
/* initialize every offset to 0 */
static uint8_t offset_table[256] = {0};
/* for each valid command, initialize the corresponding offset */
#define ENTRY(a,b,c) offset_table[c] = offsetof(offset_struct_t, b);
COMMAND_TABLE
#undef ENTRY
여기서 offsetof는 "stddef.h"에 정의 된 표준 라이브러리 매크로입니다.
부수적 인 이점으로, 명령 코드가 지원되는지 여부를 결정하는 매우 쉬운 방법이 있습니다.
bool command_is_valid(uint8_t command)
{
/* return false if not valid, or true (non 0) if valid */
return offset_table[command];
}
이것이 내 COMMAND_TABLE에서 명령 바이트 0을 예약 한 이유이기도합니다. "process_reserved ()"라는 하나의 함수를 만들 수 있습니다.이 함수는 잘못된 명령 바이트가 내 오프셋 테이블에 인덱싱하는 데 사용되는 경우 호출됩니다.
X-Macros는 기본적으로 매개 변수화 된 템플릿입니다. 따라서 여러 가지 모습으로 유사한 여러 가지가 필요한 경우 작업에 적합한 도구입니다. 추상 양식을 만들고 다른 규칙에 따라 인스턴스화 할 수 있습니다.
X- 매크로를 사용하여 열거 형 값을 문자열로 출력합니다. 그리고 그것을 만난 이후로, 나는 각 요소에 적용하기 위해 "사용자"매크로를 취하는이 양식을 강력하게 선호합니다. 다중 파일 포함은 작업하기가 훨씬 더 고통 스럽습니다.
/* x-macro constructors for error and type
enums and string tables */
#define AS_BARE(a) a ,
#define AS_STR(a) #a ,
#define ERRORS(_) \
_(noerror) \
_(dictfull) _(dictstackoverflow) _(dictstackunderflow) \
_(execstackoverflow) _(execstackunderflow) _(limitcheck) \
_(VMerror)
enum err { ERRORS(AS_BARE) };
char *errorname[] = { ERRORS(AS_STR) };
/* puts(errorname[(enum err)limitcheck]); */
또한 객체 유형에 따라 함수 디스패치에도 사용하고 있습니다. 다시 열거 형 값을 만드는 데 사용한 동일한 매크로를 하이재킹하여.
#define TYPES(_) \
_(invalid) \
_(null) \
_(mark) \
_(integer) \
_(real) \
_(array) \
_(dict) \
_(save) \
_(name) \
_(string) \
/*enddef TYPES */
#define AS_TYPE(_) _ ## type ,
enum { TYPES(AS_TYPE) };
매크로를 사용하면 매크로 정의 (TYPES 매크로)의 베어 토큰을 사용하여 다양한 형식을 구성하기 때문에 모든 배열 인덱스가 관련 열거 형 값과 일치하게됩니다.
typedef void evalfunc(context *ctx);
void evalquit(context *ctx) { ++ctx->quit; }
void evalpop(context *ctx) { (void)pop(ctx->lo, adrent(ctx->lo, OS)); }
void evalpush(context *ctx) {
push(ctx->lo, adrent(ctx->lo, OS),
pop(ctx->lo, adrent(ctx->lo, ES)));
}
evalfunc *evalinvalid = evalquit;
evalfunc *evalmark = evalpop;
evalfunc *evalnull = evalpop;
evalfunc *evalinteger = evalpush;
evalfunc *evalreal = evalpush;
evalfunc *evalsave = evalpush;
evalfunc *evaldict = evalpush;
evalfunc *evalstring = evalpush;
evalfunc *evalname = evalpush;
evalfunc *evaltype[stringtype/*last type in enum*/+1];
#define AS_EVALINIT(_) evaltype[_ ## type] = eval ## _ ;
void initevaltype(void) {
TYPES(AS_EVALINIT)
}
void eval(context *ctx) {
unsigned ades = adrent(ctx->lo, ES);
object t = top(ctx->lo, ades, 0);
if ( isx(t) ) /* if executable */
evaltype[type(t)](ctx); /* <--- the payoff is this line here! */
else
evalpush(ctx);
}
이러한 방식으로 X- 매크로를 사용하면 실제로 컴파일러가 유용한 오류 메시지를 제공하는 데 도움이됩니다. 위의 evalarray 함수는 내 요점에서 산만해질 수 있으므로 생략했습니다. 그러나 위의 코드를 컴파일하려고하면 (다른 함수 호출을 주석 처리하고 컨텍스트에 대한 더미 typedef를 제공) 컴파일러는 누락 된 함수에 대해 불평합니다. 추가하는 각각의 새 유형에 대해이 모듈을 다시 컴파일 할 때 처리기를 추가하라는 메시지가 표시됩니다. 따라서 X- 매크로는 프로젝트가 성장하더라도 병렬 구조가 그대로 유지되도록 보장합니다.
편집하다:
이 답변은 제 평판을 50 % 올렸습니다. 그래서 여기에 조금 더 있습니다. 다음은 X-Macros를 사용 하지 않을 때라 는 질문에 대한 부정적인 예입니다 .
이 예제는 임의의 코드 조각을 X- "레코드"로 압축하는 것을 보여줍니다. 나는 결국이 프로젝트의 분기를 포기하고이 전략을 이후의 디자인에서 사용하지 않았습니다. 왠지 불안해졌습니다. 실제로 매크로 이름은 X6입니다. 한 번에 6 개의 인수가 있었기 때문에 매크로 이름을 변경하는 데 지쳤습니다.
/* Object types */
/* "'X'" macros for Object type definitions, declarations and initializers */
// a b c d
// enum, string, union member, printf d
#define OBJECT_TYPES \
X6( nulltype, "null", int dummy , ("<null>")) \
X6( marktype, "mark", int dummy2 , ("<mark>")) \
X6( integertype, "integer", int i, ("%d",o.i)) \
X6( booleantype, "boolean", bool b, (o.b?"true":"false")) \
X6( realtype, "real", float f, ("%f",o.f)) \
X6( nametype, "name", int n, ("%s%s", \
(o.flags & Fxflag)?"":"/", names[o.n])) \
X6( stringtype, "string", char *s, ("%s",o.s)) \
X6( filetype, "file", FILE *file, ("<file %p>",(void *)o.file)) \
X6( arraytype, "array", Object *a, ("<array %u>",o.length)) \
X6( dicttype, "dict", struct s_pair *d, ("<dict %u>",o.length)) \
X6(operatortype, "operator", void (*o)(), ("<op>")) \
#define X6(a, b, c, d) #a,
char *typestring[] = { OBJECT_TYPES };
#undef X6
// the Object type
//forward reference so s_object can contain s_objects
typedef struct s_object Object;
// the s_object structure:
// a bit convoluted, but it boils down to four members:
// type, flags, length, and payload (union of type-specific data)
// the first named union member is integer, so a simple literal object
// can be created on the fly:
// Object o = {integertype,0,0,4028}; //create an int object, value: 4028
// Object nl = {nulltype,0,0,0};
struct s_object {
#define X6(a, b, c, d) a,
enum e_type { OBJECT_TYPES } type;
#undef X6
unsigned int flags;
#define Fread 1
#define Fwrite 2
#define Fexec 4
#define Fxflag 8
size_t length; //for lint, was: unsigned int
#define X6(a, b, c, d) c;
union { OBJECT_TYPES };
#undef X6
};
한 가지 큰 문제는 printf 형식 문자열이었습니다. 멋져 보이지만 그냥 호 쿠스 포커스입니다. 하나의 기능에서만 사용되기 때문에 매크로의 남용은 실제로 함께 있어야하는 정보를 분리합니다. 함수 자체를 읽을 수 없게 만듭니다. 난독 화는 이와 같은 디버깅 기능에서 두 배로 불행합니다.
//print the object using the type's format specifier from the macro
//used by O_equal (ps: =) and O_equalequal (ps: ==)
void printobject(Object o) {
switch (o.type) {
#define X6(a, b, c, d) \
case a: printf d; break;
OBJECT_TYPES
#undef X6
}
}
그러니 흥분하지 마십시오. 나처럼.
Java® 프로그래밍 언어 용 Oracle HotSpot Virtual Machine에를 globals.hpp사용하는 파일 RUNTIME_FLAGS이 있습니다.
소스 코드를 참조하십시오.
열거 형 값을 반복하고 각 열거 형 값에 대한 문자열 표현을 가져 오는 것을 지원하는 '풍부한 열거 형'을 만들기 위해 X 매크로를 사용하고 싶습니다.
#define MOUSE_BUTTONS \
X(LeftButton, 1) \
X(MiddleButton, 2) \
X(RightButton, 4)
struct MouseButton {
enum Value {
None = 0
#define X(name, value) ,name = value
MOUSE_BUTTONS
#undef X
};
static const int *values() {
static const int a[] = {
None,
#define X(name, value) name,
MOUSE_BUTTONS
#undef X
-1
};
return a;
}
static const char *valueAsString( Value v ) {
#define X(name, value) static const char str_##name[] = #name;
MOUSE_BUTTONS
#undef X
switch ( v ) {
case None: return "None";
#define X(name, value) case name: return str_##name;
MOUSE_BUTTONS
#undef X
}
return 0;
}
};
이것은 MouseButton::Value열거 형을 정의 할뿐만 아니라 다음과 같은 작업을 수행 할 수도 있습니다.
// Print names of all supported mouse buttons
for ( const int *mb = MouseButton::values(); *mb != -1; ++mb ) {
std::cout << MouseButton::valueAsString( (MouseButton::Value)*mb ) << "\n";
}
저는 꽤 방대한 X- 매크로를 사용하여 INI 파일의 내용을 구성 구조체로로드합니다. 무엇보다도 해당 구조체를 중심으로 회전합니다.
이것은 내 "configuration.def"파일의 모습입니다.
#define NMB_DUMMY(...) X(__VA_ARGS__)
#define NMB_INT_DEFS \
TEXT("long int") , long , , , GetLongValue , _ttol , NMB_SECT , SetLongValue ,
#define NMB_STR_DEFS NMB_STR_DEFS__(TEXT("string"))
#define NMB_PATH_DEFS NMB_STR_DEFS__(TEXT("path"))
#define NMB_STR_DEFS__(ATYPE) \
ATYPE , basic_string<TCHAR>* , new basic_string<TCHAR>\
, delete , GetValue , , NMB_SECT , SetValue , *
/* X-macro starts here */
#define NMB_SECT "server"
NMB_DUMMY(ip,TEXT("Slave IP."),TEXT("10.11.180.102"),NMB_STR_DEFS)
NMB_DUMMY(port,TEXT("Slave portti."),TEXT("502"),NMB_STR_DEFS)
NMB_DUMMY(slaveid,TEXT("Slave protocol ID."),0xff,NMB_INT_DEFS)
.
. /* And so on for about 40 items. */
약간 혼란 스럽습니다. 실제로 모든 필드 매크로 후에 모든 유형 선언을 작성하고 싶지 않다는 것이 금방 분명해집니다. (걱정하지 마십시오. 간결성을 위해 생략 한 모든 내용을 설명하는 큰 댓글이 있습니다.)
그리고 이것이 내가 구성 구조체를 선언하는 방법입니다.
typedef struct {
#define X(ID,DESC,DEFVAL,ATYPE,TYPE,...) TYPE ID;
#include "configuration.def"
#undef X
basic_string<TCHAR>* ini_path; //Where all the other stuff gets read.
long verbosity; //Used only by console writing functions.
} Config;
그런 다음 코드에서 먼저 기본값을 구성 구조체로 읽어들입니다.
#define X(ID,DESC,DEFVAL,ATYPE,TYPE,CONSTRUCTOR,DESTRUCTOR,GETTER,STRCONV,SECT,SETTER,...) \
conf->ID = CONSTRUCTOR(DEFVAL);
#include "configuration.def"
#undef X
그런 다음 SimpleIni 라이브러리를 사용하여 다음과 같이 INI를 구성 구조체로 읽어들입니다.
#define X(ID,DESC,DEFVAL,ATYPE,TYPE,CONSTRUCTOR,DESTRUCTOR,GETTER,STRCONV,SECT,SETTER,DEREF...)\
DESTRUCTOR (conf->ID);\
conf->ID = CONSTRUCTOR( ini.GETTER(TEXT(SECT),TEXT(#ID),DEFVAL,FALSE) );\
LOG3A(<< left << setw(13) << TEXT(#ID) << TEXT(": ") << left << setw(30)\
<< DEREF conf->ID << TEXT(" (") << DEFVAL << TEXT(").") );
#include "configuration.def"
#undef X
또한 동일한 이름 (GNU 긴 형식)으로 형식이 지정된 명령 줄 플래그의 재정의는 SimpleOpt 라이브러리를 사용하여 다음과 같이 간단한 방식으로 적용됩니다.
enum optflags {
#define X(ID,...) ID,
#include "configuration.def"
#undef X
};
CSimpleOpt::SOption sopt[] = {
#define X(ID,DESC,DEFVAL,ATYPE,TYPE,...) {ID,TEXT("--") #ID TEXT("="), SO_REQ_CMB},
#include "configuration.def"
#undef X
SO_END_OF_OPTIONS
};
CSimpleOpt ops(argc,argv,sopt,SO_O_NOERR);
while(ops.Next()){
switch(ops.OptionId()){
#define X(ID,DESC,DEFVAL,ATYPE,TYPE,CONSTRUCTOR,DESTRUCTOR,GETTER,STRCONV,SECT,...) \
case ID:\
DESTRUCTOR (conf->ID);\
conf->ID = STRCONV( CONSTRUCTOR ( ops.OptionArg() ) );\
LOG3A(<< TEXT("Omitted ")<<left<<setw(13)<<TEXT(#ID)<<TEXT(" : ")<<conf->ID<<TEXT(" ."));\
break;
#include "configuration.def"
#undef X
}
}
And so on, I also use the same macro to print the --help -flag output and sample default ini file, configuration.def is included 8 times in my program. "Square peg into a round hole", maybe; how would an actually competent programmer proceed with this? Lots and lots of loops and string processing?
https://github.com/whunmr/DataEx
I am using the following xmacros to generate a C++ class, with serialize and deserialize functionality built in.
#define __FIELDS_OF_DataWithNested(_) \
_(1, a, int ) \
_(2, x, DataX) \
_(3, b, int ) \
_(4, c, char ) \
_(5, d, __array(char, 3)) \
_(6, e, string) \
_(7, f, bool)
DEF_DATA(DataWithNested);
Usage:
TEST_F(t, DataWithNested_should_able_to_encode_struct_with_nested_struct) {
DataWithNested xn;
xn.a = 0xCAFEBABE;
xn.x.a = 0x12345678;
xn.x.b = 0x11223344;
xn.b = 0xDEADBEEF;
xn.c = 0x45;
memcpy(&xn.d, "XYZ", strlen("XYZ"));
char buf_with_zero[] = {0x11, 0x22, 0x00, 0x00, 0x33};
xn.e = string(buf_with_zero, sizeof(buf_with_zero));
xn.f = true;
__encode(DataWithNested, xn, buf_);
char expected[] = { 0x01, 0x04, 0x00, 0xBE, 0xBA, 0xFE, 0xCA,
0x02, 0x0E, 0x00 /*T and L of nested X*/,
0x01, 0x04, 0x00, 0x78, 0x56, 0x34, 0x12,
0x02, 0x04, 0x00, 0x44, 0x33, 0x22, 0x11,
0x03, 0x04, 0x00, 0xEF, 0xBE, 0xAD, 0xDE,
0x04, 0x01, 0x00, 0x45,
0x05, 0x03, 0x00, 'X', 'Y', 'Z',
0x06, 0x05, 0x00, 0x11, 0x22, 0x00, 0x00, 0x33,
0x07, 0x01, 0x00, 0x01};
EXPECT_TRUE(ArraysMatch(expected, buf_));
}
Also, another example is in https://github.com/whunmr/msgrpc.
참고URL : https://stackoverflow.com/questions/6635851/real-world-use-of-x-macros
'Program Club' 카테고리의 다른 글
| 재귀 적으로 정의 된 목록 이해 (zipWith 측면에서 fibs) (0) | 2020.11.10 |
|---|---|
| SQL Server의 고유 키 위반-오류 2627을 가정해도 안전합니까? (0) | 2020.11.10 |
| Haskell 함수 실행 시간 (0) | 2020.11.10 |
| Rails 앱에서 httparty로 기본 인증을 사용하는 방법은 무엇입니까? (0) | 2020.11.10 |
| composer로 개발 종속성을 제거하는 방법이 있습니까? (0) | 2020.11.10 |