메뉴 건너뛰기


Developer > Open Source

SpiderMonkey 3장. 사용자 함수 제작하기

2013.11.16 02:28

푸우 조회 수:8845

2장에서 우리는 SpiderMonkey를 사용하는 기본 방법에 대해 알아 보았습니다.
이제 부터 자신의 프로그램에서 제공하는 스크립트 엔진에 살을 붙여가면서 하나씩 확장해 나가도록 하겠습니다.
 
 
3장. 사용자 함수 제작하기
 
여기서 이야기 하는 사용자 함수란 자바스크립트에서 내가 제공하기 원하는 특별한 기능을 갖는 자바스크립트용 함수를 만들어 보겠다는 것 입니다.
 
1. 네이티브 함수(Native Function) 만들기
 
자바스크립트에서 제공하는 함수를 C/C++에서 구현한 것을 네이티브함수(Native Function)라고 합니다.
네이티브 함수를 SpiderMonkey의 jsapi를 이용하여 만들때 다음과 같은 형태로 만들어야 합니다.
 
JSBool 함수명(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval);
 
위와 같은 형태로 만들어야 SpiderMonkey가 제대로 호출 할 수 있다는 것 이죠.
이때 네이티브 함수의 리턴값은 JSBool 입니다.
함수자체의 수행이 성공했는지 여부를 JSBool형의 값인 JS_TRUE 혹은 JS_FALSE 값을 리턴해야 한다는 거죠.
이 리턴값이 자바스크립트 코드에서의 리턴값으로 사용되는 것은 아닙니다.
자바스크립트 코드에서 받을 수 있는 리턴값은 제일 마지막 아큐먼트인 rval에 넘겨주어야 합니다.
cx와 obj는 이 함수가 자바스크립트 코드에서 호출될 때의 환경에 해당하는 값이 넘어 오구요.
argc와 argv는 자바스크립트 코드에서 제공되는 함수이다보니 호출 될 때 넘겨진 아큐먼트의 수와 해당 값이 들어 옵니다.
즉, rval은 [OUT]아큐먼트고 나머지는 모두 [IN] 아큐먼트입니다.
설명할께 좀 많네요.
우선 코드를 보고 설명하겠습니다.
MyJsPrint라는 JS Native 함수는 문자열을 아큐먼트로 받아 화면에 출력해 주는 간단한 함수 입니다.
 

035: JSBool MyJsPrint(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) 
036: { 
037:         char *str=NULL;
038:         
039:         JS_ConvertArguments(cx, argc, argv, "s/", &str);
040:         if(str) {
041:                 (*rval)=INT_TO_JSVAL(printf("%s", str));
042:         } else {
043:                 (*rval)=INT_TO_JSVAL(0);
044:                 return JS_FALSE;
045:         }
046:         return JS_TRUE;
047: }
 
39라인: 자바스크립트에서 MyJsPrint함수가 호출될 경우 인자로 넘겨진 값을 받아오는 코드입니다. 
MyJsPrint함수는 문자열 인자 하나를 받아 화면에 출력해 주는 함수입니다. printf와 같이 포멧 문자가 없어도 자바스크립트 자체에서 숫자를 문자와 +연산시키면 자동으로 문자로 만들어 주기 때문에 굳이 여러 인자를 받을 필요는 없겠죠? JS_ConvertArguments()함수도 설명할 꺼리가 쫌 되므로 나중에 자세히 설명하겠습니다. 여기서는 char *str에 인자를 받아 온다 정도로 생각하면 될 것 입니다.
 
41라인: 받아온 문자열 str을 printf()함수를 이용하여 화면에 출력하고 있습니다. 원래 printf()함수는 출력한 바이트 수를 리턴함으로 우리가 만든 MyJsPrint함수도 출력한 바이트 수를 리턴할 수 있도록 하였습니다.
이 때 rval은 jsval형이므로 그냥 바이트 수를 대입할 수 없습니다.
그래서 INT_TO_JSVAL()이라는 매크로 함수를 사용하여 대입하였습니다.
INT_TO_JSVAL() 매크로 함수는 integer를 jsval형태로 변경하는 기능을 갖는 매크로 함수입니다.
이에 대해서도 jsval 데이터 타입을 설명할 때 자세히 설명하도록 하겠습니다.
 
43 ~44라인: 이런 경우는 발생하지 않을 것 같긴 하지만 str이 NULL인 경우에는 rval에 0을 대입하고 JS_FALSE를 리턴하게 하고 있습니다.
 
46라인: 함수가 정상적으로 끝나는 경우는 JS_TRUE를 리턴합니다.
 
 
자바스크립트 코드에서 함수를 호출하면서 지정한 인자들을 네이티브 함수의 argc와 argv에 담겨서 넘어 옵니다. 하지만 argv의 데이터 형이 jsval인지라 그냥 읽어 들일 수가 없습니다.
그래서 인자를 받아오려면 JS_ConvertArguments()함수를 사용하게 됩니다.
JS_ConvertArguments()함수의 원형은 다음과 같습니다.
 
JSBool JS_ConvertArguments(JSContext *cx, uintN argc, jsval *argv, const char *format, ...);
 
cx, argc, argv까지는 그냥 네이티브 함수에서 받아온 값을 그대로 넘겨주면 됩니다.
format에는 마치 sprintf의 format과 같이 특수한 문자를 지정하고 그 뒤의 ... 부분에 format에 맞춰 인자들을 받아 들일 변수들을 Call by reference형태로 적어 주면 됩니다.
format 부분에 적을 수 있는 값들은 다음과 같으며 jsval형태로 받은 인자를 C에서 사용할 수 있는 데이터 타입으로 자동 변환해 줍니다.
 

포맷문자

값을 변환하기 위해 대응되는 JS type

b

 JSBool

c

 uint16 (16-bit, unsigned integer)

i

 int32 (32-bit, ECMA-compliant signed integer)

u

 uint32 (32-bit, ECMA-compliant, unsigned integer)

j

 int32 (32-bit, signed integer)

d

 jsdouble

I

 jsdouble (converted to an integer value)

s

 JSString (treated as an array of characters)

S

 JSString

o

 JSObject

f

 JSFunction

*

 None. If an asterisk (*) is present in format, it tells the conversion routine to skip converting the current argument.

/

 None. If a slash (/) is present in format, it tells the conversion routine to turn off checking that the argument vector was passed to JS_ConvertArguments from a valid native JS function.
 
asterisk(*)는 해당 아큐먼트의 변환을 건널뛸 수 있게 하는 것인데 사용할 일은 없을 듯 합니다.
slash(/)는 /뒤쪽에 있는 모든 아큐먼트는 옵션으로 존재할 수도 있고 없을 수도 있다는 의미입니다.
예로 "s/u" 라고 format문자를 지정한다면 첫번째 아큐먼트는 문자열이고 두번째 아큐먼트는 부호 없는 정수형을 받는다는 의미입니다. 단, 부호 없는 정수형은 / 다음에 있으므로 옵션이여서 있을 수도 있고 없을 수도 있다는 것 입니다. 일단 /가 지정되면 / 뒤의 모든 아큐먼트는 옵션으로 처리가 됩니다.
 
위의 코드에서 INT_TO_JSVAL에 대한 내용을 설명하기 위해서는 jsval과 JS 데이터 타입에 대한 이해가 필요합니다.
 
 
2. JS 데이터 타입과 jsval의 사용
 
jsval은 자바스크립트 언어의 특징을 그대로 갖고 있는 데이터 타입입니다.
즉, 자바스크립트는 변수에 어떠한 형태의 값이라도 대입할 수 있습니다. 숫자, 문자는 물론 함수, 오브젝트까지도 대입할 수 있습니다. 이에 해당하는 데이터 타입이 jsval이라 할 수 있습니다.
마치 C언어에서 void *형의 변수를 선언하고 어떤한 값이나 대입한 후 type casting하여 사용하듯이 jsval도 JS 데이터 타입의 값을 저장하고 적당히 변환하여 사용하여야 합니다.
jsval을 다룰 수 있도록 SpiderMonkey에서는 jsval 변환에 관한 각종 함수와 매크로, 매크로 함수를 제공하고 있습니다.
 
1) C/C++ 데이터 타입에서 JS 데이터 타입으로의 변환
 
BOOLEAN_TO_JSVAL(b)지정된 C true 또는 false값을 JS value로 변환
DOUBLE_TO_JSVAL(dp)지정된 JS double 를 JS value로 캐스팅
INT_TO_JSVAL(i)지정된 integer값을 JS integer value로 변환
OBJECT_TO_JSVAL(obj)지정된 JS object를 JS value로 캐스팅
PRIVATE_TO_JSVAL(p)private data pointer를 JS integer value로 캐스팅
STRING_TO_JSVAL(str)지정된 JS string을 JS value로 캐스팅
 
 
2) JS 데이터 타입에서 C/C++ 데이터 타입으로의 변환
 
JSVAL_TO_BOOLEAN(v)JS value를 C의 true또는 false값으로 변환. v는 JSVAL_BOOLEAN값으로 가정합니다.
JSVAL_TO_DOUBLE(v)지정된 JS value v를 JS doouble로 캐스팅함. JS value v를 캐스팅만 할 뿐이지 v값 자체를 double형으로 바꾸지 않습니다. v값 자체를 double형으로 바꾸고 싶다면 JS_ValueToNumber()함수를 사용해야 합니다.
JSVAL_TO_INT(v)JS integer값을 integer로 변환.
JSVAL_TO_OBJECT(v)JS value를 JS object로써 캐스팅 한 값의 포인터 리턴
JSVAL_TO_PRIVATE(v)JS value를 private data pointer로 캐스팅
JSVAL_TO_STRING(v)JS value를 JS string으로써 캐스팅 한 값의 포인터 리턴
JSVAL_TO_GCTHING(v)
Clears the type tag for specified JS value, so that the JS value can be garbage collected if it is a
string, object, or number.
 
 
3) JS 데이터 타입 상수
 
JSVAL_NULLnull JS 값 정의 = OBJECT_TO_JSVAL(0)
JSVAL_TRUEtrue JS Boolean 값 정의 = BOOLEAN_TO_JSVAL(JS_TRUE)
JSVAL_FALSEfalse JS Boolean 값 정의 = BOOLEAN_TO_JSVAL(JS_FALSE)
JSVAL_ONE1의 JS 값 정의 = INT_TO_JSVAL(1)
JSVAL_VOIDvoid JS 값 정의 = 0-JSVAL_INT_POW2(30)
JSVAL_ZERO0의 JS 값 정의 = INT_TO_JSVAL(0)
 
 
4) JS 데이터 타입 확인
 
JSVAL_IS_BOOLEAN(v)지정된 값이 JS Boolean(JSVAL_BOOLEAN)데이터 타입이라면 true 리턴
JSVAL_IS_DOUBLE(v)지정된 JS value가 JS double(JSVAL_DOUBLE) 데이터 타입이라면 true 리턴
JSVAL_IS_INT(v)지정된 값이 JS integer(JSVAL_INT) 데이터 타입이라면 true 리턴
JSVAL_IS_NULL(v)지정된 값이 null(JSVAL_NULL)인 경우 true 리턴
JSVAL_IS_NUMBER(v)지정된 값이 JS integer(JSVAL_INT) 또는 JS double(JSVAL_DOUBLE) 값이라면 true 리턴
JSVAL_IS_OBJECT(v)지정된 값이 JS object(JSVAL_OBJECT)라면 true 리턴
JSVAL_IS_PRIMITIVE(v)
주어진 JS value가 primitive 타입이라면 true 리턴. primitive들은 undefined, null, boolean, numeric,
string 타입의 값을 말합니다.
JSVAL_IS_STRING(v)지정된 JS value가 JS string 데이터 타입(JSVAL_STRING)이라면 true 리턴
JSVAL_IS_VOID(v)지정된 JS value가 void(JSVAL_VOID)라면 true 리턴
INT_FITS_IN_JSVAL(i)지정된 C integer 값 i가 JS integer로 표현할 수 있는 범위의 값이라면 true 리턴.
JSVAL_IS_GCTHING(v)
Indicates whether a
JS value has a type that is subject to garbage collection.
 
  
5) JS 데이터 타입 관련 함수
 
JS_ValueToBoolean(cx,v,bp)JS value를 JS Boolean로 변환
JS_ValueToECMAInt32(cx,v,ip)JS value를 ECMA-compliant, 32-bit integer로 변환
JS_ValueToECMAUint32(cx,v,ip)JS value를 ECMA-compliant, 부호없는 32-bit integer로 변환
JS_ValueToFunction(cx,v)JS value를 JS function로 변환
JS_ValueToId(cx,v,idp)JS value를 JS ID로 변환
JS_ValueToInt32(cx,v,ip)JS value를 JS 32-bit integer로 변환
JS_ValueToNumber(cx,v,dp)JS value를 JS double로 변환
JS_ValueToObject(cx,v,objp)JS value를 JS object로 변환
JS_ValueToString(cx,v)JS value를 JS string으로 변환
JS_ValueToUint16(cx,v,ip)JS value를 부호없는 16-bit integer로 변환
JS_ConvertValue(cx,v,type,vp)JS value를 지정한 JS type의 값으로 변환. type은 JSTYPE_VOID, JSTYPE_OBJECT, JSTYPE_FUNCTION, JSTYPE_STRING, JSTYPE_NUMBER, JSTYPE_BOOLEAN 중 하나
JS_IdToVlaue(cx,v,bp)JS ID를 JS vlaue로 변환
JS_TypeOfValue(cx,v)JS value의 JS data type을 리턴. JSTYPE_VOID, JSTYPE_OBJECT, JSTYPE_FUNCTION, JSTYPE_STRING, JSTYPE_NUMBER,  JSTYPE_BOOLEAN 중 하나의 값을 리턴
  
엄청 많죠? jsval관련 함수와 매크로들이 jsapi 중에 약 30%는 차지하는 것 같네요.
어찌되었건 C/C++ 데이터 타입의 값들을 위에 나열한 함수들을 사용하여 jsval에 대입하고 또 jsval을 위에 나열한 함수들을 이용하여 C/C++ 데이터 타입의 값으로 캐스팅 하거나 변환하여 사용하면 됩니다.
 
3. 글로벌 함수 만들기
 
우리는 이전 장에서 SpiderMonkey의 기본 사용 골격을 보았습니다.
그 때 글로벌 오브젝트를 생성하고 그 글로벌 오브젝트에 자바스크립트에서 기본으로 제공하여야 하는 클래스들을 등록하였습니다. (ECMA표준에 Array, Date, Math 등을 기본 클래스로 제공하라고 되어 있어서 기본 클래스라고 하였습니다.)
글로벌 오브젝트에 속한 함수나 프로퍼티 등은 다른 오브젝트와는 달리 이름을 지정하고 하고 해당 함수나 프로퍼티를 바로 사용할 수 있습니다.
즉, 글로벌 오브젝트에 등록한 표준 클래스인 Date객체는 자바스크립트에서 Global.Date 와 같이 사용하는 것이 아니라 그냥 Date라고 사용하면 된다는 것 입니다. 
그래서 자바스크립트 전반적으로 사용하는 그런 함수를 글로벌 함수로 만들어 제공하면 좋을 것 입니다.
이번 절에서 만들 글로벌 함수는 그냥 화면에 문자열을 출력하는 함수를 만들어 보는 것 입니다.
이미 이야기 한 것 처럼 SpiderMonkey는 ECMA에서 정한 것 외에는 아무것도 기본으로 제공하고 있지 않습니다.
그래서 화면에 뭔가를 찍기 위한 함수 조차도 없어요. (ㅠㅠ)
어찌 되었건 이번에 이런 함수를 만들면 실제로 사용하지 않더라도 디버깅 용으로라도 유용할 듯 합니다.
위에서 만들었던 JS Native 함수를 활용하도록 하죠.
위에서 만들었던 MyJsPrint()라는 함수를 글로벌 오브젝트에서 제공하기 위해서는 JSFunctionSpec배열을 만들어 글로벌 오브젝트에 등록시켜 주어야 합니다.
 
1) JS Function 정의 하기
 
JSFunctionSpec 배열은 다음과 같은 방법으로 만들 수 있습니다. 
 

072: #define JS_FS(name,call,nargs,flags,extra)      {(name),(call),(nargs),(flags),(extra)}
073: #define JS_FS_END                               {0,0,0,0,0},
074: 
075: static JSFunctionSpec GlobalFunc[] = { 
076:         JS_FS("print", MyJsPrint, 1, 0, 0), 
077:         JS_FS("println", MyJsPrintln, 1, 0, 0), 
078:         JS_FS_END
079: };
 
76라인: JS_FS()는 매크로 함수로써 구조체의 값을 순서대로 적을 수 있도록 되어 있습니다.
첫번째 값은 자바스크립트에서 MyJsPrint라는 함수를 어떤 이름으로 제공할지를 문자열로 적습니다.
즉, 위에서 보면 C/C++코드에서는 MyJsPrint()라는 함수명을 갖고 있지만 이것이 자바스크립트에서 사용할때는 print()라는 함수명으로 사용된다는 것 있입니다.
두번째 값은 JS Native 함수의 주소(즉, 함수명)를 적습니다.
세번재 값은 제공하는 print()함수가  최소 몇개의 아큐먼트를 입력받아야 하는지를 적습니다.
네번째 값은 제공하는 함수가 갖는 속성을 지정하는 것인데 0이면 속성을 가지고 있지 않다는 것이고 값을 지정하고 싶은 경우는 JSFUN_BOUND_METHOD, JSFUN_GLOBAL_PARENT 값을 사용합니다.
그런데 SpiderMonkey에서는 여기에 옵션을 주는 것을 권장하지 않습니다. 옛날 소스와의 호환을 위해서 남겨 놓는 것 뿐이랍니다. 그래서 의미를 파악하기 보다는 그냥 0을 주십시오.
다섯번째 값은 현재는 사용되지 않고 미래를 위해 준비된 필드입니다. 그래서 그냥 0 주세요.
 
여기까지 써 놓고 다시 문서를 보니깐...
SpiderMonkey 1.5.x까지는 세번째, 네번째 값을 0으로 사용하는 것으로 되어 있었는데...
SpiderMonkey 1.7.x로 가면서 세번째 네번째 값을 사용하고 있네요.
세번째 값은 프로퍼티 속성, 함수 플래그 그리고 JSFUN_STUB_GSOPS 를 bitwise OR 할 수 있습니다.
함수플래그에는 JSFUN_FAST_NATIVE와 JSFUN_BOUND_METHOD가 있는데 JSFUN_BOUND_METHOD는 이젠 사용하지 않는다고 하고 JSFUN_FAST_NATIVE는 1.8에 추가될 예정입니다.
네번재 값은 원래는 uint16 이였는데 지금은 uint32로 바뀌었으며 상위16비트는 0으로 주면 되고 하위 16비트는 the number of extra local roots the function desires, available at argv[MAX(nargs, argc)] onward.
하지만 일반적으로는 세번째, 네번째 모두 0으로 주면 됩니다.
 
77라인: MyJsPrint()함수는 입력으로 받는 문자열을 그대로 출력하는 함수인데 똑같은 함수의 printf()의 제일 마지막에 "\n"문자하나 붙여서 MyJsPrintln()이라는 함수를 하나 더 만들었습니다.
입력받은 문자열에 개행문자를 추가하여 출력하는 함수입니다. 코드의 예는 구지 보여드리지 않겠습니다.
 
78라인: JS_FS_END는 구조체의 모든 항목을 0으로 채워서 배열이 끝났음을 알려주는 것 뿐입니다.
 
2) 글로벌 오브젝트에 함수 등록하기
 
print()나 println()함수를 글로벌 함수로 자바스크립트에서 사용하기 위해서는 앞에서 만든 JSFunctionSpec 배열인 GlobalFunc를 글로벌 오브젝트에 등록해 주셔야 합니다.
등록하는 방법은 아래와 같으며 스크립트를 컴파일 하기 전에 아래의 구문을 실행해 주시면 됩니다.
 

112:                 JS_DefineFunctions(cx, global, GlobalFunc);
 
JS_DefineFunctions()의 두번째 인자로 등록한 오브젝트의 주소를 넘겨주고 세버째 인자에 JSFunctionSpec을 주면 됩니다.
이를 잘 생각해 보면 글로벌 함수가 아니라 어떤 객체의 함수를 만들고 싶다면 global 대신 해당 객체 주소를 넣어 주면 되겠죠.
 
여기까지 하면 자바스크립트에서 print()나 println()함수를 이용해서 원하는 곳에서 화면에 원하는 내용을 출력할 수 있습니다. 즉, 이전에는 스크립트의 최종 라인의 리턴값을 C에서 찍어 보는 방식이였는데 이제 스크립트 내에서 원하는 위치에서 값을 찍어 볼 수 있다는 거죠.
 
연습 삼아 아래와 같은 스크립트 코드를 생성하여 한번 돌려 보도록 합시다.
 
 
println("Hello Embeded Javascript World by Spider Monkey");
 
println("오늘은 "+Date()+"입니다.");
 
for(i=2; i<=9; i++) {
    println("========== "+i+"단 ==========")
    for(j=1; j<=9; j++) {
        println(i+" x "+j+" = "+(i*j));
    }
}
 
실행했을 때 출력하길 원했던 문자열과 오늘의 날짜 그리고 구구단이 화면에 잘 보였다면 성공한 겁니다. 모두들 성공하셨길 바랍니다.
 
이번 강좌는 기초 개념을 같이 설명해야 해서 쫌 길어 졌네요.
 
드디어 글쓰기에 대한 귀차니즘이 오기 시작하네요.
아무튼 다음번 강좌때 다시 만나죠.
 
 
 
Creative Commons License
Creative Commons License이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-동일조건변경허락 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
Copyright 조희창(Nicholas Jo). Some rights reserved. http://bbs.nicklib.com