Article:RuleBasedSpiritFrom IdeA thinKING
Rule-based Spirit 사용법이번 강좌에서는 복잡한 문법을 parsing할 경우에도 사용할 수 있는 rule-based spirit 사용법에 대해 알아보겠습니다. 예제먼저 이번장에서는 간단한 메신저 프로그램에서 사용할 수 있는 파일 형식을 만든다고 가정합니다. 내부적으로 사용하는 데이터 구조는 다음과 같습니다. enum status_e { SIGNOUT, BUSY, AVAILABLE }; struct contact_t { std::string id; std::string name; std::string email; std::string ip4; status_e sts; }; 이 데이터 구조를 저장하기 위해 아래와 같은 파일 형식을 사용합니다. iwongu = { iwongu@abc.com, "Lee Wongoo", LOGOUT, };
gdhong = { gdhong@foo.bar.com, "Hong Gilldong", BUSY, 127.0.0.1:5060 };
물론 아래와 같이 줄을 바꾸거나 마음대로 띄워쓰기도 가능합니다. iwongu = {
iwongu@abc.com,
"Lee Wongoo",
LOGOUT,
};
gdhong = {
gdhong@foo.bar.com,
"Hong Gilldong",
BUSY,
127.0.0.1:5060
};
EBNF 정의위에서 정의한 문법을 EBNF로 표현해 보면 다음과 같습니다. (밑에서부터 읽으면 편합니다.) digit ::= [0-9]
alpha ::= [a-zA-Z]
alnum ::= alpha | digit
status ::= alpha+
port ::= ':' digit+
ip4 ::= digit+ '.' digit+ '.' digit+ '.' digit+ [port]
domainName ::= ip4 | domain
domain ::= nameString
nameString ::= (alpha) (alnum | '-' | '.')*
quotedString ::= '"' (anychar - '"')* '";
name ::= quotedString
email ::= nameString '@' domainName
id ::= nameString
contact ::= id '=' '{' email ',' name ',' status ',' [ip4] '}' ';'
contacts ::= contact*
참고로 위의 EBNF는 실제로 domain이나 ip4 형식을 정확히 parsing 하지는 않습니다. 예제를 위해 필요한 정도로만 정확합니다.
boost::spirit::grammar, rule먼저 다음과 같이 grammar 클래스를 상속받는 클래스를 선언합니다. struct contact_list : public boost::spirit::grammar<contact_list> { template <typename ScannerT> struct definition { definition(contact_list const& self) { using namespace boost::spirit; ... (1) } boost::spirit::rule<ScannerT> contacts, ... (2); boost::spirit::rule<ScannerT> const& start() const { return contacts; } }; }; 여기서 start()함수는 parser가 parsing을 시작할 rule을 지정합니다. 위의 코드에서는 parser가 contacts라는 rule을 가지고 parsing을 시작하도록 하고 있습니다. 그럼 위의 EBNF 형식을 가지고 ...(1)번 부분을 구현해보면 다음과 같습니다. status = +alpha_p; port = ch_p(':') >> +digit_p; ip4 = +digit_p >> '.' >> +digit_p >> '.' >> +digit_p >> '.' >> +digit_p >> !port; domainName = ip4 | domain; domain = nameString; nameString = alpha_p >> *(alnum_p | '-' | '.'); quotedString = '"' >> *(~ch_p('"')) >> '"'; name = quotedString; email = nameString >> '@' >> domainName; id = nameString; contact = id >> '=' >> '{' >> email >> ',' >> name >> ',' >> status >> ',' >> !ip4 >> '}' >> ';'; contacts = *contact; 위의 spirit 구문에서 ! 기호는 optional 을 나타냅니다. 마지막으로 ...(2)번 항목에 위에서 사용한 rule들의 이름을 선언합니다. boost::spirit::rule<ScannerT> contacts, contact, id, email, name, quotedString, nameString, domain, domainName, ip4, port, status; 이것으로 parser는 완성입니다. 생각보다 쉽죠? 이제 이 parsing한 결과를 이용하기 위해 action들을 사용할 차례입니다. 잠시... 사소한 개선 사항들action들을 사용하기에 앞서 위의 rule들을 confix_p와 c_escape_ch_p를 사용하여 조금 개선해봅시다. 먼저 confix_p는 opening, expression, closing으로 이루어진 문법을 위해 사용합니다. 예를 들어 "( ... )"나 "/* ... */" 등과 같은 문법을 위해 사용합니다. confix_p를 사용하는 것이 사용하지 않는 것과 어떤 차이가 있는지는 [1]를 참고하세요. 위의 경우 이 문법에 해당하는 경우가 두 군데 있습니다. 하나는 contact를 정의하기 위해 사용된 '{', '}' 문법이고 나머지 하나는 quotedString을 정의하기 위해 사용된 '"', string, '"' 부분입니다. 이를 confix_p를 사용하여 다시 정의하면 다음과 같습니다. quotedString = confix_p('"', (*c_escape_ch_p), '"'); contact = id >> '=' >> confix_p('{', email >> ',' >> name >> ',' >> status >> ',' >> !ip4, '}') >> ';'; 여기서 c_escape_ch_p는 C언어에서 사용되는 것과 같은 문자열 escape문자를 인식하여 처리해줍니다. [2] actions이제 parsing 결과를 action과 연결시킬 차례입니다. 여기서도 inline usage와 같이 바로 assign_a, push_back_a와 같은 action을 바로 연결시킬 수 있습니다. 하지만 이렇게 문법안에 action이 hard-coding되면 애써 작성한 문법의 재사용이 불가능해집니다. 따라서 action을 정의하는 클래스를 인자로 받아들일 수 있는 문법을 정의하여 재사용성을 높여 보겠습니다. 먼저 action 클래스를 인자로 받아야 하므로 contact_list 선언을 template을 사용하여 수정합니다. 그리고 action 객체를 reference로 받아 저장하는 생성자를 다음과 같이 선언합니다. template <class Action> struct contact_list : public boost::spirit::grammar<contact_list<Action> > { contact_list(Action& action_) : action(action_) { } template <typename ScannerT> struct definition { definition(contact_list const& self) { using namespace boost::spirit; Action& action = self.action; ... } ... }; Action& action; }; 그리고 아래 코드처럼 필요한 부분에 적당히 action을 호출하는 코드를 넣어줍니다. contact = id[action.id] >> '=' >> confix_p( '{', email[action.email] >> ',' >> name[action.name] >> ',' >> status[action.status] >> ',' >> !ip4[action.ip4], '}') >> ';'; contacts = *contact[action.contact]; 물론 위의 parser를 사용하려면 위의 문법에 맞도록 action 클래스를 만들어주어야 합니다. 위의 코드를 살펴 보면 위의 문법과 같이 사용할 action 클래스는 id, email, name, status, ip4, 그리고 contact이라는 action을 제공해야 함을 알 수 있습니다. 그럼 첫번째로 파일을 읽어 줄을 맞춰주는 간단한 beautifier action 클래스를 만들어보겠습니다. beautifier이번에 만들 beautifier action 클래스는 파일 안의 내용이 어떤 들여쓰기나 newline을 사용하더라도 각줄에 한 항목씩 들어가도록 줄을 맞춰주는 action 클래스입니다. 즉, 위쪽의 파일 예제의 첫번째 형식을 입력받아 두번째 형식으로 출력해주는 action입니다. 먼저 contact_list grammar에서 요구하는 action은 id, email, name, status, ip4, contact, 이렇게 여섯개이므로 이 여섯개 action의 틀을 구현합니다. rule에 대한 action은 입력된 문자열에서 현재 parsing된 부분을 iterator쌍으로 받게 되며 다음과 같이 작성하면 됩니다. struct beautifier { struct id_a { template <typename IteratorT> void operator()(IteratorT first, IteratorT last) const { } } id; struct email_a { template <typename IteratorT> void operator()(IteratorT first, IteratorT last) const { } } email; ... }; 그럼 이 틀에 실제 beautifier의 기능을 할 수 있는 코드를 넣어보겠습니다. 여기서는 코드를 간단히 하기 위해 결과를 std::cout으로 출력하도록 하였습니다. struct beautifier { struct id_a { template <typename IteratorT> void operator()(IteratorT first, IteratorT last) const { std::cout << std::string(first, last) << " = {\n"; } } id; struct email_a { template <typename IteratorT> void operator()(IteratorT first, IteratorT last) const { std::cout << '\t' << std::string(first, last) << ",\n"; } } email; ... name, status, ip4의 구현은 email과 같습니다. struct contact_a { template <typename IteratorT> void operator()(IteratorT first, IteratorT last) const { std::cout << "};\n"; } } contact; }; 위의 action들중 email_a에서 ip4_a까지는 모두 같은 일을 합니다. 따라서 이 경우 하나의 action을 가지고 다음과 같이 간단히 구현할 수 있습니다. struct beautifier { struct id_a { template <typename IteratorT> void operator()(IteratorT first, IteratorT last) const { std::cout << std::string(first, last) << " = {\n"; } } id; struct comma_a { template <typename IteratorT> void operator()(IteratorT first, IteratorT last) const { std::cout << '\t' << std::string(first, last) << ",\n"; } } email, name, status, ip4; struct contact_a { template <typename IteratorT> void operator()(IteratorT first, IteratorT last) const { std::cout << "};\n"; } } contact; }; 컴파일 후 실행시키면 어떤 형식으로 파일을 작성했건 문법에 맞게만 작성했다면 다음과 같은 형식으로 변경되어 출력됩니다. iwongu = {
iwongu@abc.com,
"Lee Wongoo",
SIGNOUT,
};
gdhong = {
gdhong@foo.bar.com,
"Hong Gilldong",
BUSY,
127.0.0.1:5060,
};
Symbol tableSymbol table은 특정 symbol과 그와 관련된 값을 가지고 있는 테이블입니다. Spirit에서는 dynamic symbol table를 지원하여 parsing도중에 동적으로 symbol을 추가할 수도 있습니다. 처음에 살펴보았듯이 예제에는 status_e라는 enum 값이 정의되어 있습니다. 하지만 현재 parser에서는 이 enum값을 parsing해주는 것이 아니라 단순히 문자열로써 인식하고 있습니다. 만약 이 enum 값을 parser가 인식하여 enum값으로 변경하여 전달해준다면 사용자는 더욱 쉽게 parser를 사용할 수 있게 될 것입니다. 따라서 이 enum 값을 symbol table을 사용하여 인식하도록 해보겠습니다. 여기서는 symbol table의 동적인 관리는 사용하지 않습니다. 먼저 다음과 같이 symbol table을 정의합니다. 보시는 바와 같이 spirit의 symbols 클래스를 사용합니다. struct statusST : boost::spirit::symbols<status_e> { statusST() { add ("SIGNOUT" , SIGNOUT) ("BUSY" , BUSY) ("AVAILABLE" , AVAILABLE) ; } }; 여기서는 생성자에서 각 symbol을 적당한 enum 값과 정적으로 연결시킵니다. 다음으로 문법 부분에서 기존의 status 부분을 다음과 같이 변경합니다. template <class Action> struct contact_list : public boost::spirit::grammar<contact_list<Action> > { ... template <typename ScannerT> struct definition { definition(contact_list const& self) { ... contact = id[action.id] >> '=' >> confix_p( '{', email[action.email] >> ',' >> name[action.name] >> ',' >> status_p[action.status] >> ',' >> !ip4[action.ip4], '}') >> ';'; ... } statusST status_p; ... }; ... }; 다음으로 symbol table에 matching되는 action의 타입은 연결된 값을 받도록 구현해야 합니다. 따라서 beautifier의 status를 다음과 같이 수정합니다. struct beautifier { ... struct status_a { void operator()(status_e sts) const { std::cout << '\t' << status2str(sts) << ",\n"; } } status; ... }; 여기서 status2str() 함수는 enum 값을 문자열로 바꿔주는 helper function입니다. 이렇게 symbol table을 사용함으로써 얻을 수 있는 장점중의 하나는 각 symbol들의 약어들을 사용하도록 쉽게 확장할 수 있다는 것입니다. 예를 들어 symbol table에 아래와 같은 entry를 추가하면 다음과 같은 파일 형식도 인식할 수 있습니다. struct statusST : boost::spirit::symbols<status_e> { statusST() { add ... ("S/O" , SIGNOUT) ("B/S" , BUSY) ("A/V" , AVAILABLE) ; } }; iwongu = { iwongu@abc.com, "Lee Wongoo", S/O, };
gdhong = { gdhong@foo.bar.com, "Hong Gilldong", B/S, 127.0.0.1:5060 };
위 파일을 beautifier로 parsing한 결과는 위에서 나온 결과와 같습니다. contact_callback그럼 마지막으로 위에서 만든 문법을 가지고 beautifier 외에 다른 action클래스를 만들어봄으로써 문법의 재사용성을 확인해 보겠습니다. 이번에는 파일을 parsing하면서 하나의 항목을 인식하면 인식한 결과를 contact_t 객체에 담아 이를 인자로 action 클래스에 사용자가 등록한 callback함수를 호출하는 클래스를 만들어보겠습니다. 먼저 사용자가 callback 함수를 등록할 수 있도록 하는 action 클래스는 다음과 같습니다. 여기서는 callback 함수의 타입으로 boost::function을 사용하였습니다. [3] 그리고 사용한 컴파일러가 preferred syntax를 지워하지 않아 portable syntax를 사용했습니다. [4] struct contact_callback { typedef boost::function1<void, const contact_t&> callback_t; contact_callback(callback_t cb_) : cb(cb_), id(data.id), email(data.email), name(data.name), ip4(data.ip4), status(data.sts), contact(cb, data) { } struct assign_a { assign_a(std::string& ref_) : ref(ref_) { } template <typename IteratorT> void operator()(IteratorT first, IteratorT last) const { ref = std::string(first, last); } std::string& ref; }; struct status_a { status_a(status_e& ref_) : ref(ref_) { } void operator()(status_e sts) const { ref = sts; } status_e& ref; }; struct contact_a { contact_a(callback_t cb_, contact_t& data_) : cb(cb_), data(data_) { } template <typename IteratorT> void operator()(IteratorT first, IteratorT last) const { cb(data); } callback_t cb; contact_t& data; }; callback_t cb; contact_t data; assign_a id, email, name, ip4; status_a status; contact_a contact; }; 먼저 생성자에서는 사용자가 등록한 callback함수를 저장하고 id와 같은 action들을 멤버 변수인 data의 적당한 필드와 연결시켜주는 작업을 합니다. 이때 한가지 신경써야 할 점은 contact의 초기화시에 멤버 변수 cb가 사용되므로 이 cb가 먼저 초기화된 후 contact 이 초기화되어야 한다는 점입니다. 즉, struct 선언시에 cb가 contact보다 윗쪽에 위치해야 합니다. [5] 마지막으로 contact_a action은 저장된 data 객체를 가지고 callback 함수를 호출합니다. 그럼 이 action 클래스를 사용하는 callback 함수를 작성해 보겠습니다. 아래의 callback 함수가 하는 일은 위에서 살펴본 beautifier 과 같습니다. 다만 이전의 beautifier는 각 action 들에서 맡은 부분만을 출력했지만 이번 callback 함수는 contact_t 객체를 받아 이를 출력한다는 점이 다릅니다. struct callback { void operator()(const contact_t& ct) { std::cout << ct.id << " = {\n\t" << ct.email << ",\n\t" << ct.name << ",\n\t" << status2str(ct.sts) << ",\n\t" << ct.ip4 << "\n" << "}\n"; } }; 마지막으로 위의 callback함수를 action 클래스에 등록한 후 이 action 클래스를 가지고 parsing을 합니다. 물론 결과는 위의 beautifier 결과와 동일하게 출력됩니다. bool do_parse(const char* buff) { using namespace std; using namespace boost::spirit; contact_callback::callback_t f = callback(); contact_callback action(f); contact_list<contact_callback> parser(action); parse_info<> info = parse(buff, parser, space_p); if (!info.full) { cout << ">> parse error" << endl; cout << info.full << endl; cout << info.hit << endl; cout << info.length << endl; cout << info.stop << endl; } return info.full; } 결론이번 강좌에서는 boost::spirit의 사용법에 대해 살펴보았습니다. 처음 접근하기에는 조금 난이도가 있으나 조금만 익숙해지면 여러 상황에서 유용하게 사용할 수 있습니다. 이번에 살펴본 내용은 boost::spirit의 사용법의 일부로 더욱 많은 내용이 Spirit|User's Guide에 있습니다. 예를 들어 lambda expression을 구현한 Phoenix는 간단한 action을 만드는데 편리하게 사용할 수 있습니다. 그럼 이것으로 boost::spirit 강좌를 마치겠습니다. 긴 글 읽어주셔서 감사합니다. |

