티스토리 뷰

: 오픈 소스 기여의 첫걸음으로 C로 구현된 JSON parser project인, jsmn의 분석을 해보려 한다. 

사실 jsmn(jasmine)은 워낙 유명하기도 하고 코드 자체 완성도가 높아 기여할 여지는 적다.  

하지만 전체 코드가 그리 길지 않아, 오픈소스 분석과 기여를 연습하기 위해서는 꽤 적절하다. 






0. jsmn 프로젝트에 대하여

: jsmn은 기존 JSON parser가 불필요한 기능을 제공한다는 점에서 착안해 더 robust, fast, portable, simple한 parser를 만들고자 시작했다고 한다. simplicity가 핵심인 듯 하다. 먼저 JSON의 token에 대한 정의를 먼저 하려 한다.


* token striing을 구분할 수 있는 물리적 단위. 


1) token은 이 아닌, 범위를 가지는 개념이다. 


2) 임의의 JSON object  { "name" : "Jack", "age" : 27}

위는 여러개의 token으로 생각할 수 있다. 

Object [0..31], String [3..7], String [12..16], String [20..23], Number [27..29]


3) < json token의 종류 >

          • Object - a container of key-value pairs, e.g.: { "foo":"bar", "x":0.3 }
          • Array - a sequence of values, e.g.: [ 1, 2, 3 ]
          • String - a quoted sequence of chars, e.g.: "foo"
          • Primitive - a number, a boolean (truefalse) or null
4) jsmn에서 정의된 token관련 type들에 대해 살펴보면 더 자세히 알 수 있다. 
typedef enum {
	JSMN_UNDEFINED = 0,
	JSMN_OBJECT = 1,
	JSMN_ARRAY = 2,
	JSMN_STRING = 3,
	JSMN_PRIMITIVE = 4
} jsmntype_t;

-> 위 코드는 token type을 표현하는 enum이며, 얘는 토큰 변수인 jsmntok_t의 구성요소로 역할하게 되겠다. 


typedef struct {
	jsmntype_t type; // Token type
	int start;       // Token start position
	int end;         // Token end position
	int size;        // Number of child (nested) tokens
} jsmntok_t;

-> 위는 token변수. type, 시작 위치, 끝 위치, child의 갯수로 구성된다. 

( child의 갯수를 담고 있는 이유는 token이 token을 포함하는 경우가 발생할 수 있기 때문이다. )








1. 사전 작업 

: fork해서 내 repository로 가져온 후, vi도 연습할 겸 교내 우분투 서버 계정으로 땡겨왔다. 







2. 쪼개기

: 이제부터 원본 소스의 변수 단위, 함수 단위로 쪼개어 기능과 역할에 대한 분석을 해보겠다. 

(분석 대상 : jsmn.c, jsmn.h)


1) 변수 정리 

이름

타입 

의미 

 [type] jsmntype_t

 enum (object, array, string, primitive)

 json type identifier

 json의 5가지 type를 구분하기 위함. 

(object, array, string, primivie, 하나는 undefined)

 [type]  jsmntok_t

 struct (jsmntype_t, start, end, size)


 json token변수. 

 type, token의 시작위치, 끝위치, child token들의 갯수를 담고 있다.


 token은 의미 단위다. 



 [type]  jsmn_parser

 struct (pos, toknext, toksuper)

 pos : 주어진 JSON input STRING 내에서 위치를 가리키는 역할 수행. (하나하나 조사하면서 계속 움직임)


 toknext : 다음 token이 시작될 위치. 


 toksuper : 상위 token의 위치를 가지고 있음.

 parser는 token간의 위치를 지정한다. 


현재 token, 다음 token, 조상 token. 


 각 token의 범위를 지정해 줌으로써 의미를 뽑아내는 작업(parsing)이 수행된다. 


--------------------------

 parser는 전체 JSON input string에 대해 하나인 것이 아니라, 개별 token마다 하나씩 가지고 있어야 한다는 것 눈치챌 수 있어야 한다. 



2) 함수 정리  

: 어느 library나 그렇듯, 함수의 중요도가 동등하진 않은것 같다. 그래서 핵심이 되는 함수들 위주로 자세히 분석하고, 전체적으로 간단히 정리하기로 했다.



  1. jsmn_parse() 


1) parameters 분석


jsmn_parse()는 주어진 json string에 대하여 실제 파싱을 수행하는 함수다. 

그래서 현재 token위치와 다음 token의 위치를 가리키고 있을 parser와, 

조사할 전체 string pointer와 전체 length, 

만들어진 token을 담을 tokens배열 (author는 128개를 최대로 봤음)과 token의 전체 갯수 정보를 받아온다. 


2) 동작 

: parse 함수는 기본적으로 string내의 char를 하나하나 빼내서 일일히 다~ 조사하는 과정을 거친다. 


1. 공통 작업


: tokentok_t 타입의 token을 하나 만들고,  

  cf) jsmntok_t = jsmntype_t + start위치 + end위치 + size + parent위치 

   parser가 가리키는 현재 pos의 char하나를 빼온다. 

      c = js[parser->pos];


2. 빼온 c에 대하여 switch문을 적용

: 아래는 switch문에 대한 각 case를 분류한 결과다.


  1) {, [  


: {, [  는 obejct나, array를 시작하는 문자이므로, 위에서 만들어 놓은 token을 하나 할당하고, type, start위치, super token을 지정해준다. 


  2) }. ]  

: }, ]의 경우 token의 ERROR 여부를 검출하는데 사용되는 것 같다. 

해당 library에서 정의한 Error는 크게 3가지다. 



  3) \"  (string) 


: string의 경우 처리할 경우의 수가 많아, sub함수인 jsmn_parse_string() 함수를 정의해서 사용. (아래에 있음)




  4) \t, \r, \n 을 비롯한 :, , 등 

: 위와 같은 것들이 단독으로 사용될 경우 무시하게 되어있다. 

(주의: quote(")안에서 string의 일부로 사용된 \t, \r, \n 등은 jsmn_parse_string()에서 따로 처리해 주고 있다! )



  5) number, boolean 등 (STRICT) 


: jsmn에서 특이한 부분인데, 이 library에서는 number, boolean, null 등을 한꺼번에 싸그리 모아서 STRICT PRIMITIVE라고 분류한다. 이렇게 분류하는 이유는, key-value pair에서 object(token)의 key가 될 수 없기 때문이라고 한다. 원래 json에서 해당 char로 시작할 수 없나보다. 


  6) 나머지 경우 ( PRIMITIVE ) 


" 에 둘러싸여 있지 않은 모든 value(STRICT MODE는 제외)의 경우 primitive로 간주하며, 이 경우도 string을 처리했던 것처럼 sub함수를 통해 추가 작업을 해준다 -> jsmn_parse_primitive() 


3) RETURN

: jsmn_parse()는 ERROR가 아닌 이상 count를 return하게 되어있다. 






  2. jsmn_parse_string() 


1) parameters 분석

: jsmn_parse()의 sub함수로, 동일한 parameter를 쓴다. 

 

2) 동작 

: 크게 두가지에 대해 case 구분을 해놨고, 나머지는 unexpected char로 간주해 ERROR를 낸다. 

return은 성공여부를 반환! 


  1) " (quote) 



: " 의 경우 string이 끝난 것으로 간주하고, token의 정보들(start, pos) 등을 최종적으로 setting해준다. 


이경우 sub로 jsmn_alloc_token()와 jsmn_fill_token()를 call하는데, 별건 없고 그냥 초기화 해주고, 값을 변수에 하나하나 대입해주는 getter함수 비슷한 느낌이다. 



  2) \ (백스페이스) 



: 백스페이스인 경우, 크게 2가지 case를 뒀는데, 

첫번째 경우는, \t (tab)과 같은 문자를 인식하기 위함, 

두번째 경우는, \uXXXX 형식의 Unicode에 대응하기 위함이라고 하는데 (뭔지 잘은 모르겠다...)




  3. jsmn_parse_primitive()


1) parameters 분석

: jsmn_parse()의 sub함수로, 동일한 parameter를 쓴다. 

 

2) 동작 




: 이 함수에서 핵심은 아닌것을 걸러내는데 있는것 같다. 즉, ERROR인 상황을 다 걸러내고, 나머지는 다 일반적으로 처리하겠다는 것. 


위에서 특히 눈에 띄는, 32, 127이 의미하는 바를 확인하기 위해 ASCII코드표를 찾아봤는데, 

     


(32보다 작은건 문자로 취급할 수 없는 것들이었고, 128 이상은 없는 숫자이므로 제외. )









* 아래는 다른 함수들에 대해 간략하게 정리해본 표다.


이름

파라미터

리턴값

수행내용

  jsmn_init

 jsmn_parser 

 -

token array로부터 parser생성

  jsmn_parse


 jsmn_parser *parser

 parser = 현재 token의 pos + next token의 pos + parent token의 pos

 const char *js

 조사할 대상 string

 size_t len

 조사할 대사 string의 전체길이

 jsmntok_t *tokens

 가용한 token배열

 unsigned int num_tokens

 가용한 token의 갯수


 json object

 json data string을 하나의 json object로 만들어줌

  jsmn_alloc_token

 parser, tokens, num_tokens

 token

 새로운 token 하나 생성.

(main parsing함수에서 사용.)

  jsmn_fill_token

 token, tokentype, 시작위치, 끝위치

 - 

 token type과 시작, 끝위치 결정. (main parsing함수에서 사용.)

  jsmn_parse_primitive

 parser, 길이, tokens, num_tokens


 성공 여부 반환


(정의된 실패 경우는 3종류. 


1. ERROR_NOMEN : 토큰이 충분치 않음


2. ERROR_INVAL : json string안에 invalid character 포함


3. ERROR_PART : json string packet의 형식이 잘못됨.)


primitive type parser. 


 

  jsmn_parse_string

 string type parser. 

  jsmn_parse

 main parser 함수. 

 {,[ 를 만나면 token count를 올리고, parser로부터 token을 생성해서 start, end위치를 잡아주고 상위 token의 관계를 확인한다. 또한 }, ]를 조사해 object가 끝날 것인지, array인지 여부를 조사한다. 


primitive type인 경우 첫 character만 보고 결정. 



 

 

 

 




3) JSON 구동 형식


{"menu": {
  "id": "file",
  "value": "File",
  "popup": {
    "menuitem": [
      {"value": "New", "onclick": "CreateNewDoc()"},
      {"value": "Open", "onclick": "OpenDoc()"},
      {"value": "Close", "onclick": "CloseDoc()"}
    ]
  }
}}

: json은 key-value pair형식이며 중첩이 가능하다. DB의 hash처럼 key 값으로 value를 얻는 간단한 형식이다. 


-> 위 예제에서, 최상위 key 'menu'에 대하여, value로 object를 취하고 있다. 

해당 object는 3개의 key 'id', 'value', 'popup'을 가지고 있는데, 각각 value를 가지고 있고, 

특히 popup은 value로 다시 object를 갖는 형식이고, 그 하위 value로 '['를 사용하여 array를 취하고 있다.




이러한 JSON의 형식을 고려하여 파싱은 다음과 같이 진행된다. 


1. { 인지 판단하여 token의 시작을 인식하고, 

2. " 로 문자열의 시작을 알게된다. (jsmn에서는 primitive type인 경우 숫자, bool, null을 나누지 않고 구현되었다.)

Note: Unlike JSON data types, primitive tokens are not divided into numbers, booleans and null, because one can easily tell the type using the first character:

3. 그리고 문자열이 끝나는 "를 인식하고, token 배열에 집어넣는다. 

























댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함