最新的基于ChromeV8引擎JS与C++互调过程(后附GitHub项目地址)。

##C++调用js方法:##
我是按照v8给的sample中的process.cc中的实例做的,因为OpenGL绘制的工作在onDrawFrame()中执行的,所以我们要确保js中绘制代码的方法在onDrawFrame()中执行,所以我们需要在js中加上一些方法,通过v8来让C++决定方法什么时候调用。比如像下面的一个js方法,逻辑应该是这样的,我们通过调用initView(这个方法是通过v8包装的,关于如何包装的下面会详细介绍这里先说逻辑)来调用我们在C++中暴露给js的绘制接口,这个方法我们需要在onDrawFrame()(jni方法)中。function initView(){draw();}
有一个重要的问题就是关于v8初始化的时机,一开始我在Activity的onCreate()中单独用一个jni方法来初始化v8,其他关于v8的操作都是在onSurfaceChanged,onDrawFrame,onSurfaceCreated的jni方法中进行的,但是当我在onDrawFrame()的jni方法调用js方法initView()来绘制view的时候总是报错。在找不到原因后我把v8初始化的时机改成在onSurfaceCreated的jni方法中,由于v8只需初始化一次,所以要在清单文件中给游戏Activity的配置必须至少为android:configChanges="orientation|keyboardHidden|screenSize";改完之后方法调用成功。这里的原因可能是v8初始化与运行的环境问题,暂时不明。下面来看一下具体逻辑C++调用js方法v8的封装逻辑:

    ```java

    v8Helper::Initialize();
    Isolate::Scope isolate_scope(v8Helper::GetIsolate());
    // Create a stack-allocated handle scope.
    HandleScope handle_scope(v8Helper::GetIsolate());
    // Create a new context.
    Local context =creatTestContext(v8Helper::GetIsolate());
    context_.Reset(v8Helper::GetIsolate(),context);
    if (context.IsEmpty())
    {
        LOGI("Error creating context\n");
    }
    // Enter the context for compiling and running the hello world script.
    Context::Scope context_scope(context);
    // Create a string containing the JavaScript source code.
    bool result =   v8Helper::ExecuteString(context->GetIsolate(),
                                            String::NewFromUtf8(context->GetIsolate(), jsSource,
                                                                  NewStringType::kNormal).ToLocalChecked(),true, true);
    LOGI("JS Script Execute Result :%d", result);
    //调用js方法

    Local process_name =
            String::NewFromUtf8(v8Helper::GetIsolate(), "initView", NewStringType::kNormal)
                    .ToLocalChecked();
    Local process_val;
    // If there is no Process function, or if it is not a function,
    if (!context->Global()->Get(context, process_name).ToLocal(&process_val) ||
        !process_val->IsFunction()) {
        LOGI("initView is not a function\n");
    }
    // It is a function; cast it to a Function
    Local process_fun = Local::Cast(process_val);
    process_.Reset(v8Helper::GetIsolate(),process_fun);
              
    ```
    ```JavaScript
     function initView(person){
        
     }

    ```

不管是js调用C++的方法,还是C++调用js的方法,对这些方法的包装都应该是v8初始化完成后最先操作的。在 ``Local context =creatTestContext(v8Helper::GetIsolate()); ``之前的操作都是v8初始化常规代码我做了一些简单的封装,这个方法主要是来绑定C++暴露给js接口的所以暂时不讨论。注意  ``context_.Reset(GetIsolate(), context); ``这一句是用来保存当前上下文句柄的, ``process_.Reset(GetIsolate(), process_fun); ``是用来保存在js找出来的方法的句柄的,这两句是我们在以后的任何时候都可以调用js方法的关键。中间的一些代码都很常规,就是找出js中的方法名然后转成方法。应该注意到找出js方法的操作是在脚本加载并且执行完之后进行的,这是因为如果在加载js脚本之前找js的方法是肯定找不到的。context_是Global类型,process_是Global类型,这两个全局类型的对象其实是用来保存当前的上下文环境和需要以后来执行的方法的句柄的。以后我们我们可以通过这两个句柄,进入到相应上下文环境中执行相应的方法。接下来看一下,我们是怎么在C++中调用在js中找出来的这个方法的,因为我们需要在onDrawFrame()的jni方法中执行绘制代码所以在onDrawFrame()的jni方法中要这样来调用:

    ``` java
        // Create a handle scope to keep the temporary object references.
    HandleScope handle_scope(v8Helper::GetIsolate());

    v8::Local context =
            v8::Local::New(v8Helper::GetIsolate(), context_);

    // Enter this processor's context so all the remaining operations
    // take place there
    Context::Scope context_scope(context);

    // Set up an exception handler before calling the Process function
    TryCatch try_catch(v8Helper::GetIsolate());

    const int argc = 0;
    Local argv[argc] = {};
    v8::Local process =
            v8::Local::New(v8Helper::GetIsolate(), process_);
    Local result;
    if (!process->Call(context, context->Global(), argc, argv).ToLocal(&result)) {
        String::Utf8Value error(v8Helper::GetIsolate(), try_catch.Exception());
        LOGI("call js function error:%s",*error);
    }
      
    ```

首先是创建一栈区域来保存当前临时对象引用。
红框内的方法就是把之前保存在 ``context_ ``,和 ``process_ ``对象中的句柄拿出来,先进入相应的上下文,然后在对应的上下文环境中拿到对应的方法来执行。基本C++调用js方法都是这个套路,这里argc为0是调用无参的js方法,调用有参的js方法,我还没试不过按照process.cc中的套路也应该没什么问题。
##js调用C++的方法:##

js调用C++的方法就比较容易了

    ```java
    v8Helper::Initialize();
    Isolate::Scope isolate_scope(v8Helper::GetIsolate());
    // Create a stack-allocated handle scope.
    HandleScope handle_scope(v8Helper::GetIsolate());
    // Create a new context.
    Local context =creatTestContext(v8Helper::GetIsolate());
    context_.Reset(v8Helper::GetIsolate(),context);
    if (context.IsEmpty())
    {
        LOGI("Error creating context\n");
    }
    // Enter the context for compiling and running the hello world script.
    Context::Scope context_scope(context);
    // Create a string containing the JavaScript source code.
    bool result =   v8Helper::ExecuteString(context->GetIsolate(),
                                            String::NewFromUtf8(context->GetIsolate(), jsSource,
                                                                NewStringType::kNormal).ToLocalChecked(),true, true);
    LOGI("JS Script Execute Result :%d", result);
    ```

可以看到在创建一个新的上下文环境的时候这里直接creatTestContext(v8Helper::GetIsolate());其实是做了两个操作如下

    ```java
     // Creates a new execution environment containing the built-in functions.
   
    v8::Local CreateShellContext(v8::Isolate* isolate) {
     // Create a template for the global object.
    v8::Local global = v8::ObjectTemplate::New(isolate);
    // Bind the global 'print' function to the C++ Print callback.
    global->Set(
      v8::String::NewFromUtf8(isolate, "print", v8::NewStringType::kNormal)
          .ToLocalChecked(),
      v8::FunctionTemplate::New(isolate, Print))

    // Bind the global 'read' function to the C++ Read callback.
    global->Set(v8::String::NewFromUtf8(
                  isolate, "read", v8::NewStringType::kNormal).ToLocalChecked(),
              v8::FunctionTemplate::New(isolate, Read));
    
     }
    ```

    ```JavaScript
      var x = [6, 8, 'JS', 6*8];
      print(x);
      
    ``


首先创建一个全局对象的模板然后绑定C++的方法到字符串上,js就可以用此字符串来调用绑定的方法。应该注意到这些绑定在执行js字符串之前就已经进行了。
##js调C++类##
这里有两种情况:

1. 在C++中创建类的对象,然后把对象作为参数传递给js,在js中访问对象的属性和方法。
2. 在js中创建对象,在JS中访问对象的属性和方法。
 
第一种情况:
这种情况下与调用C++普通方法的区别是js在调用C++方法的时候多了一个对象参数,由于这个参数是个C++类的对象,所以我们需要把这个C++对象封装成js对象。

    ```java
     // Create a handle scope to keep the temporary object references.
    HandleScope handle_scope(v8Helper::GetIsolate());

    v8::Local context =
            v8::Local::New(v8Helper::GetIsolate(), context_);

    // Enter this processor's context so all the remaining operations
    // take place there
    Context::Scope context_scope(context);

    Person person("SexMonkey",18);
    Local person_object = WrapPerson(&person);

    // Set up an exception handler before calling the Process function
    TryCatch try_catch(v8Helper::GetIsolate());

    const int argc = 1;
    Local argv[argc] = {person_object};
    v8::Local process =
            v8::Local::New(v8Helper::GetIsolate(), process_);
    Local result;
    if (!process->Call(context, context->Global(), argc, argv).ToLocal(&result)) {
        String::Utf8Value error(v8Helper::GetIsolate(), try_catch.Exception());
        LOGI("call js function error:%s",*error);
    }
    ```
****
    ```java
     Local WrapPerson(Person *person) {
    // Local scope for temporary handles.
    EscapableHandleScope handle_scope(v8Helper::GetIsolate());
    // Fetch the template for creating JavaScript person wrappers.
    // It only has to be created once, which we do on demand.
    if (person_template_.IsEmpty()) {
        Local raw_template = MakePersonTemplate(v8Helper::GetIsolate());
        person_template_.Reset(v8Helper::GetIsolate(), raw_template);
    }
    Local temp = Local::New(v8Helper::GetIsolate(),person_template_);
    // Create an empty  person wrapper.
    Local result = temp -> NewInstance(v8Helper::GetIsolate() -> GetCurrentContext()).ToLocalChecked();
    // Wrap the raw C++ pointer in an External so it can be referenced
    // from within JavaScript.
    Local person_ptr = External::New(v8Helper::GetIsolate(),person);

    // Store the person pointer in the JavaScript wrapper.
    result->SetInternalField(0, person_ptr);
    // Return the result through the current handle scope.  Since each
    // of these handles will go away when the handle scope is deleted
    // we need to call Close to let one, the result, escape into the
    // outer handle scope.
    return handle_scope.Escape(result);
    }
    ```

****

    ```java

    Local MakePersonTemplate(Isolate *isolate) {

    EscapableHandleScope handle_scope(isolate);

    Local result = ObjectTemplate::New(isolate);
    result->SetInternalFieldCount(1);

    // Add accessors for each of the fields of the request.
    result->SetAccessor(
            String::NewFromUtf8(isolate, "name", NewStringType::kInternalized)
                    .ToLocalChecked(),
           GetName);
    result->SetAccessor(
            String::NewFromUtf8(isolate, "age", NewStringType::kInternalized)
                    .ToLocalChecked(),
            GetAge);

    result->Set(String::NewFromUtf8(isolate, "setName", NewStringType::kNormal)
                        .ToLocalChecked(),
                FunctionTemplate::New(isolate, SetName));
    result->Set(String::NewFromUtf8(isolate, "setAge", NewStringType::kNormal)
                        .ToLocalChecked(),
                FunctionTemplate::New(isolate, SetAge));

    // Again, return the result through the current handle scope.
    return handle_scope.Escape(result);
    }

    ```

****
    ```java

    void GetName(Local name, const PropertyCallbackInfo& info) {
    // Extract the C++ request object from the JavaScript wrapper.
    Person* person = UnwrapPerson(info.Holder());

    // Fetch the path.
    const std::string& cName = person -> getName();

    // Wrap the result in a JavaScript string and return it.
    info.GetReturnValue().Set(
            String::NewFromUtf8(info.GetIsolate(), cName.c_str(),
                                NewStringType::kNormal,
                                static_cast(cName.length())).ToLocalChecked());
    }
    ```

****
    ```java
    void SetAge(const FunctionCallbackInfo &args)
    {
    LOGI("setAge is called");
    Person* person = UnwrapPerson(args.Holder());
    person -> setAge(args[0]->Uint32Value());

    }
    ```

****
    ```java
    Person* UnwrapPerson(Local obj) {
    Local field = Local::Cast(obj->GetInternalField(0));
    void* ptr = field->Value();
    return static_cast(ptr);
    }
    ```

    ```JavaScript
    function initView(person){
       person.setName("JerryZhu");
       person.setAge(25);
       person.name;
    }
    ```
仔细看一下其实套路都是一样的,只不过是多了几步针对C++对象封装的专有步骤而已。主要的步骤就是先创建一个对象模板为这个对象模板绑定暴露给js访问C++对象的属性和方法的接口,注意对访问属性和访问方法的封装是不一样的,虽然这些套路都是固定的但也要注意其中的区别。

以我传递的person对象为例,当我在js中用person.name来访问person的name属性的时候,v8实际上就会调用GetName(Local name, const PropertyCallbackInfo& info)方法,通过回调的info参数来解包装,转成你传递的对像类型的指针后,再用这个指针来访问对象的成员方法getName()来获取name的值,然后再设置成返回值其实也就是person.name的值。访问方法也一样,比如在js中访问person.setName("123"),v8会调用SetName(const FunctionCallbackInfo &args);也是先解包装转换成你传递的对像类型的指针后再用对象的成员方法setName(*str(args.GetIsolate(),args[0]));通过v8回调给C++的参数来改变对象的属性值。注意以上person.name,person.setName("123"),name和setName都是你绑定对象模板时暴露给js接口的字符串,person也是你自己在js中使用的一个字符而已,你也可以用teacher,teacher.name。在模板创建完成后又经过了几个步骤主要是对刚才创建的模板与C++对象的关联。这些步骤完成后,一个C++对象就封装成为js对象了,然后把这个对象当做参数传给js,js就可以访问之前创建的模板上绑定的方法了。

第二种情况:
在js中创建C++的对象去访问对象的属性和方法,重点是对构造函数的绑定,绑定的时机与一般函数即全局函数一样,在js文件加载之前就可绑定,注意是在creatTestContext(Isolate *isolate)这个方法中进行绑定。

    ```java

    Local creatTestContext(Isolate *isolate) {
    Local global = ObjectTemplate::New(isolate);
    //Create the function template for the constructor, and point it to our constructor,
    Handle person_template = FunctionTemplate::New(v8Helper::GetIsolate(),PersonConstructor);
    //We can tell the scripts what type to report. In this case, just Person will do.
    person_template->SetClassName(String::NewFromUtf8(v8Helper::GetIsolate(), "Person", NewStringType::kNormal)
                                           .ToLocalChecked());
    //This template is the unique properties of this type, we can set
    //functions, getters, setters, and values directly on each new Person() object.
    Handle person = person_template -> InstanceTemplate();
    //Again, this is to store the c++ object for use in callbacks.
    person -> SetInternalFieldCount(1);
    person -> SetAccessor(
            String::NewFromUtf8(isolate, "name", NewStringType::kInternalized)
                    .ToLocalChecked(),
            GetName);
    person -> SetAccessor(
            String::NewFromUtf8(isolate, "age", NewStringType::kInternalized)
                    .ToLocalChecked(),
            GetAge);

    person -> Set(String::NewFromUtf8(isolate, "setName", NewStringType::kNormal)
                        .ToLocalChecked(),
                FunctionTemplate::New(isolate, SetName));
    person -> Set(String::NewFromUtf8(isolate, "setAge", NewStringType::kNormal)
                        .ToLocalChecked(),
                FunctionTemplate::New(isolate, SetAge));

    //Finally, we can tell the global scope that it now has a 'function' called Person,
    //and that this function returns the template object above.
    global -> Set(String::NewFromUtf8(isolate, "Person", NewStringType::kNormal)
                          .ToLocalChecked(),person_template);

    return Context::New(isolate,0,global);
    }

    ```
****

    ```java 

    void  PersonConstructor(const FunctionCallbackInfo & args){
    LOGI("PersonConstructor is called");
    if (!args.IsConstructCall())
    {
        LOGI("args is not PersonConstructor call");
    }

    Handle object = args.This();
    HandleScope handle_scope(v8Helper::GetIsolate());
    String::Utf8Value str(args.GetIsolate(),args[0]);
    std::string name = *str;
    int age = args[1] -> Uint32Value();
    Person *person =  new Person(name,age);

    //Note that this index 0 is the internal field we created in the template!
    object -> SetInternalField(0,v8::External::New(v8Helper::GetIsolate(),person));
    }

    ```JavaScript
    var per = new Person("JIMI",20);
    print(per.name,per.age);
 
    ```

在creatTestContext(Isolate *isolate)中先是为构造函数创建函数模板,并将其指向我们的构造函数,SetClassName是告诉js脚本C++对象的类型,就是来区分普通字符串和C++类的,InstanceTemplate()是获取C++实例模板,接下来就是为js中创建C++对象访问其属性和方法绑定的C++接口,与我们传递C++对象给js函数然后用对象访问属性和方法的绑定过程是一样的,最后一步是设置person(name,age)这个全局函数给js调用;
构造函数的绑定需要注意一下,回调参数args数组索引对应着是初始化对象时的属性顺序,拿到属性值后,用这些属性值在C++中创建一个类的对象,然后再把对象指针设置到索引为0的地方,v8会在js中C++对象调用其属性和方法时从这个地方查询到真正的C++对象。

##C++调js类##
C++调用调用js的类与C++调用js方法有些许类似,都是在脚本加载并运行之后进行的。看一下代码会发现调用过程有点复杂,但基本套路都是一样的。

    ```java 
    v8Helper::Initialize();
    Isolate::Scope isolate_scope(v8Helper::GetIsolate());
    // Create a stack-allocated handle scope.
    HandleScope handle_scope(v8Helper::GetIsolate());
    // Create a new context.
    Local context =creatTestContext(v8Helper::GetIsolate());
    context_.Reset(v8Helper::GetIsolate(),context);
    if (context.IsEmpty())
    {
        LOGI("Error creating context\n");
    }

    // Enter the context for compiling and running the hello world script.
    Context::Scope context_scope(context);
    // Create a string containing the JavaScript source code.
    bool result =   v8Helper::ExecuteString(context->GetIsolate(),
                                            String::NewFromUtf8(context->GetIsolate(), jsSource,NewStringType::kNormal).ToLocalChecked(),true, true);

    
    LOGI("JS Script Execute Result :%d", result);

    /*C++调用js类 start..*/
    Local js_data = String::NewFromUtf8(v8Helper::GetIsolate(), "Point", NewStringType::kInternalized)
            .ToLocalChecked();
    Local js_data_value = context -> Global() -> Get(js_data);
    String::Utf8Value str(js_data_value);
    LOGI("Point = %s \n",*str);
    bool  isFunction = js_data_value -> IsFunction();
    LOGI("Point is function %d",isFunction);
    bool  isObject = js_data_value -> IsObject();
    LOGI("Point is object %d",isObject);
    Local js_data_object = Local::Cast(js_data_value);
    // var newObj = new Point(1,2);
    const int argc = 2;
    Local argv[argc] = {};
    argv[0] = Int32::New(v8Helper::GetIsolate(),7);
    argv[1] = Int32::New(v8Helper::GetIsolate(),8);
    Local newObject = js_data_object -> CallAsConstructor(context, argc, argv).ToLocalChecked();
    LOGI("Point is function %d \n",newObject -> IsFunction());
    LOGI("Point is object %d",newObject -> IsObject());
    // newObj.show();
    Local obj = Local::Cast(newObject);
    Local js_func_name = String::NewFromUtf8(v8Helper::GetIsolate(), "show", NewStringType::kInternalized).ToLocalChecked();
    Local  js_func_ref = obj->Get(js_func_name);
    Local js_func = Local::Cast(js_func_ref);
    js_data_value = js_func->Call(obj, 0, NULL) ;

    String::Utf8Value str2(js_data_value);
    LOGI("Point = %s \n",*str2);

    /*C++调用js类 end..*/

    //调用js方法
    Local process_name =
            String::NewFromUtf8(v8Helper::GetIsolate(), "initView", NewStringType::kNormal)
                    .ToLocalChecked();
    Local process_val;
    // If there is no Process function, or if it is not a function,
    if (!context->Global()->Get(context, process_name).ToLocal(&process_val) ||
        !process_val->IsFunction()) {
        LOGI("initView is not a function\n");
    }
    // It is a function; cast it to a Function
    Local process_fun = Local::Cast(process_val);
    process_.Reset(v8Helper::GetIsolate(),process_fun);

        
    ```
    ```JavaScript
     function Point(x,y){
        this.x=x;
        this.y=y;
    }
    Point.prototype.show=function(){
       return '(x,y) = '+this.x+','+this.y;
    }
 
    ```

首先将想要调用的js类的类名以字符串的形式转成v8能识别的字符串,然后``context -> Global() -> Get(js_data)``, v8开始通过这个类名在js中找到相应的值,然后转通过相应的方法来先看看这个找出来的值是方法还是类,我们可以在脚本中看到这个值既是类又是方法,这其实是个构造方法,然后``Local::Cast(js_data_value);``将这个值转化成了js的类,接着``js_data_object -> CallAsConstructor(context, argc, argv).ToLocalChecked();``,是调用js类的构造方法并做一些属性的初始化操作,返回的就是js类的对象了,接下来就是用这个对象来调用js类的方法了,与之前C++调用js全局方法的方法是一样的。注意prototype是JavaScript类的一个关键字它可以指定类的属性,脚本中是把show()方法当做类的方法。可以用类的对象调用这个方法。

GitHub项目地址:

https://github.com/Sexmonkey/smartV8.git

另外本人还有一个没有使用OpenGL,只是展示了基于最新的v8引擎C++和JS交互逻辑的项目:

https://github.com/Sexmonkey/v8demo.git

你可能感兴趣的:(最新的基于ChromeV8引擎JS与C++互调过程(后附GitHub项目地址)。)