Unity에서 JavaScript Plugin 만들기 #2 - 기능 이해하기 (콜백)

FGPRJS·2022년 5월 16일
0

Unity JavaScript Plugin

목록 보기
3/4

Unity에서 제공하는 문서는 단순히 함수나 변수에 관한 것으로서, 그다지 유용하지 않다. 저기서 사용되는 수준은 기존의 C#으로도 충분히 할 수 있는 코드이고(굳이 JavaScript로 할 필요가 없음) 다른 언어를 사용해야 할 정도로 중요한 객체나 콜백을 다루는 방법등에 대해서는 다루지 않기 때문.

emscripten 공식 문서에서 이 규칙을 상세하게 제공하지만, 이것을 처음부터 보기엔 난이도가 있고, 거의 대부분이 지금 당장 사용하지 않을 기능이기 때문에, 필요한 부분만 순서대로 확인하려고 한다.


다음 링크에서 제공하는 예시문을 살펴본다.

  • C# 예시 문서
using System;
using System.Runtime.InteropServices;
using AOT;
using UnityEngine;
 
public class JsCallCsTest : MonoBehaviour
{
    [DllImport("__Internal")]
    public static extern void ProvideCallback(Action action);
 
    public void Start()
    {
        ProvideCallback(Callback);
    }
 
    [MonoPInvokeCallback(typeof(Action))]
    public static void Callback()
    {
        Debug.Log("Callback called");
    }
}
  • JS 예시 문서
var LibraryJsCallCsTest = {
    $JsCallCsTest: {},
 
    ProvideCallback: function(obj)
    {
        console.log("ProvideCallback");
        console.log(obj);
        JsCallCsTest.callback = obj;
        Runtime.dynCall('v', obj, 0);
    },
};
 
autoAddDeps(LibraryJsCallCsTest, '$JsCallCsTest');
mergeInto(LibraryManager.library, LibraryJsCallCsTest);
  1. [MonoPInvokeCallback(typeof(Action))] 라는 attribute가 붙어있는 Callback이라는 함수가 있다.

  2. DllImport(여기서는 jslib)안에서 ProvideCallback을 가져온다.

  3. 이 Instance가 Start할 때, 2.의 DllImport로 갖고온 ProvideCallback을 1.의 Callback객체(함수)를 Export할 JsCallCsTest라는 객체의 callback이라는 프로퍼티로 한다.

  4. 그 다음, Runtime.dynCall을 사용한다. 무언가 argument가 있지만, 무엇을 뜻하는지는 모른다.

어떤 방식으로 작성되는지는 대충 알겠지만, 여전히 모르는것이 많다.


다른 사이트에서 제공하는 이해하기 쉬운 설명을 참조한다.

Runtime.dynCall(signature,functionPtr,arguments);

이는 보일러플레이트 코드(반복되는 코드)이며, JavaScript에서 C# 함수를 사용할 수 있게 한다.

파라미터의 의미는 다음과 같다.

  • signature (type : string)
    • return type followed by each argument type.

    • 리턴타입과 이후의 파라미터 타입순이다.
      v : void
      i : int
      f : float
      d : double

    • '들'이라고 하였다. 여러개가 붙어서 나올 수 있다.

    • 예시1) 상기 기재된 예시

      [MonoPInvokeCallback(typeof(Action))]
      public static void Callback()
      {
          Debug.Log("Callback called");
      }

      위 함수를 가져오려면 "v"를 사용해야 한다.
      (리턴타입 void, 파라미터 없음)

    • 예시2)

      public static void SomeCallback(int id, string data)

      위 함수를 가져오고 싶다면, "vii"를 사용해야 한다.
      분명히 마지막 파라미터는 string이지만, 처리되는 것은 string에 대한 포인터라서 int로 받는다고 한다.

  • functionPtr (type : function)
    • 발동할 C# 함수의 포인터
  • arguments (type : array)
    • 발동할 C#함수에게 건네주고 싶은 인자들.

링크에서 제공한 예제는 다음과 같다.

public class JSAPI{
    
    [DllImport("__Internal")]
    private static extern void JSExample(Action callback);

    [MonoPInvokeCallback(typeof(Action))]
    public static void DefaultCallback(){
        //This fires from javascript
    }

    public void YourMethod(){
        JSExample(DefaultCallback);
    }
}
var myLib={
    $dependencies:{},
    JSExample: function(functionPtr){
        Runtime.dynCall("v",functionPtr,[]);
    }
};
autoAddDeps(myLib,'$dependencies');
mergInto(LibraryManager.library,myLib);
  • myLib 개체가 export할 대상이다.
  • export할 대상 중 JSExample은 인자로 받은 functionPtr이 가리키는 return type이 void에 추가 파라미터는 없는 함수를 아무 인자도 주지 않고 발동한다.
  • C# 코드에서, JSExample은 jslib에서 가져오는 함수이며, JavaScript jslib으로부터 JSExample을 가져와 DefaultCallback 함수를 주면서 발동한다.

Unity는 C#에서 wasm으로 바꾸는 과정 중 emscripten을 사용한다. 2019년 버전의 emscripten 부터, JavaScript 함수를 invokable한 method 포인터로 wrap할 수 있는 기능을 추가했다.

emscripten은 v.1.38.26 버전부터 다음과 같은 기능을 제공한다.

Runtime.addFunction(jsFunction,signature):IntPtr
  • jsfunction (object:function)
    • C# 함수로 래핑하고 싶은 JavaScript 함수
  • signature (string)
    • 상기 signature와 같음. 리턴타입을 맨처음, 그 이후로는 함수의 인자들의 타입
  • return:IntPtr (int)
    • C#에게 넘길 수 있는 Integer 포인터

예시는 다음과 같다.

//상기의 JSExample 코드

 JSCallbackExample: function(){
        var callback=function(){
            alert("Received a callback!");
        };
        var ptr=Runtime.addFunction(callback,'v');
        return ptr;
    }

//상기의 JSExample 코드
[DllImport("__Internal")]
private static extern Action JSCallbackExample();

void ExampleUsage(){
    Action a = JSCallbackExample();
    a.Invoke();
}
  • js 코드에서 callback 함수를 정의한다.
  • ptr은 Runtime.addFunction으로 방금 만든 callback을 함수로 만들고, 그 함수의 ptr을 갖고 있는다.
  • 그 함수 포인터를 리턴하는 과정이 담긴 JSCallbackExample코드
  • JSCallbackExample 코드를 C#에서 가져온다.
  • ExampleUsage함수를 발동하면, Action으로 JSCallbackExample 함수를 발동한 결과 (JavaScript함수의 포인터)를 가져오고, 그것을 Invoke한다.

Emscripten에서 제공하는 Dependency 관련 정보

JavaScript libraries can declare dependencies (__deps), however those are only for other JavaScript libraries.
자바스크립트 라이브러리는 dependency들을 선언할 수 있지만(__deps 키워드를 뒤에 붙이는 것으로), 이것은 JavaScript 라이브러리끼리나 가능한 것이다. (즉, C, C++, C#과는 다름)

You can add dependencies for all your methods using autoAddDeps(myLibrary, name) where myLibrary is the object with all your methods, and name is the thing they all depend upon.
모든 메소드를 감싸는 myLibrary 개체가 있을 때, autoAddDeps(myLibrary, name)함수를 사용해 모든 함수들에 의존성을 추가할 수 있다. name이 의존할 개체의 이름이다.

This is useful when all the implemented methods use a JavaScript singleton containing helper methods.
이 기능은 모든 구현된 메소드들이 JavaScript 싱글톤 헬퍼 메소드를 사용할 때 유용하다.

예제를 다시 확인해본다.

public class JSAPI{
    
    [DllImport("__Internal")]
    private static extern void JSExample(Action callback);

    [MonoPInvokeCallback(typeof(Action))]
    public static void DefaultCallback(){
        //This fires from javascript
    }

    public void YourMethod(){
        JSExample(DefaultCallback);
    }
}
var myLib={
    $dependencies:{},
    JSExample: function(functionPtr){
        Runtime.dynCall("v",functionPtr,[]);
    }
};
autoAddDeps(myLib,'$dependencies');
mergInto(LibraryManager.library,myLib);

이 예시를 보면, 최고 myLib 개체에 $dependencies가 있고, 그것을 의존하여 추가한다.


참고할 만한 자료들

- JSWebsocket jslib을 만들어 놓은 github

profile
FGPRJS

0개의 댓글