SpiderMonkey 入门

原文地址:https://developer.mozilla.org/en-US/docs/SpiderMonkey/JSAPI_User_Guide

文章为郭胜龙所写,转载说明出处


本文是mozilla官网上关于spiderMonder的一篇用户指南,正好要用,顺带翻译了一下:


JSAPI 用户指南

全局对象:全局对象包括所有可被JS代码用的类,方法,变量。例如当js代码进行了类似window.open的操作,它会获取一个全局的属性,在这个例子中是window。JSAPI对可见的全局属性脚本有全权的控制。该应用程序通过创建一个对象,并用标准的JavaScript类类似Array和Object填充它来开始,然后它再把应用程序希望提供的自定义类、函数和变量(类似window);每次应用程序运行JS脚本(例如使用JS_EvaluateScript),它会提供一个全局的对象供这个脚本使用。当脚本运行时它会创建自己的全局的函数和变量。所有这些函数和变量以属性的形式储存在全局对象中。


一个简单的例子:

3个关键元素中每个都需要一些JSAPI调用:

runtime:使用JS_NewRuntime来创建并使用JS_DestroyRuntime来销毁。当你的应用不使用了,使用JS_ShutDown来释放所有缓存资源。(每次结束都调用是个好习惯!!!)

context:使用 JS_NewContext and JS_DestroyContext。为了保证ECMAScript 标准的一致性,应用也需要使用JS_SetOptions来开启JSOPTION_VAROBJFIX。为了获得JS的新功能,应用可能会使用JS_SetVersion。错误报告也是每个context都需要并且通过启用JS_SetErrorReporter来使用。

golbal object:你需要一个设置了JSCLASS_GLOBAL_FLAGS选项的JSClass。下面的例子定义了一个非常简单的JSClass(叫global_class)没有自己的属性和 方法。使用JS_NewGlobalObject来创建一个全局对象。使用JS_InitStandardClasses来用标准的JS全局变量填充它。

JSAPI一般设计应用程序的规模会有多个线程,多个contexts和多个全局对象。它是细粒度的API,支持各种部分的组合,给应用对SpiderMonkey行为的绝对掌控。

下面是一个小例子,包含上面讨论的所有东西:

#include "jsapi.h"

/* The class of the global object. */
static JSClass global_class = { "global",
                                JSCLASS_NEW_RESOLVE | JSCLASS_GLOBAL_FLAGS,
                                JS_PropertyStub,
                                JS_DeletePropertyStub,
                                JS_PropertyStub,
                                JS_StrictPropertyStub,
                                JS_EnumerateStub,
                                JS_ResolveStub,
                                JS_ConvertStub,
                                NULL,
                                JSCLASS_NO_OPTIONAL_MEMBERS
};
/* The error reporter callback. */
void reportError(JSContext *cx, const char *message, JSErrorReport *report) {
     fprintf(stderr, "%s:%u:%s\n",
             report->filename ? report->filename : "[no filename]",
             (unsigned int) report->lineno,
             message);
}
int run(JSContext *cx) {
    /* Enter a request before running anything in the context */
    JSAutoRequest ar(cx);

    /* Create the global object in a new compartment. */
    JSObject *global = JS_NewGlobalObject(cx, &global_class, NULL);
    if (global == NULL)
        return 1;

    /* Set the context's global */
    JSAutoCompartment ac(cx, global);
    JS_SetGlobalObject(cx, global);

    /* Populate the global object with the standard globals, like Object and Array. */
    if (!JS_InitStandardClasses(cx, global))
        return 1;

    /* Your application code here. This may include JSAPI calls to create your own custom JS objects and run scripts. */

    return 0;
}
int main(int argc, const char *argv[]) {
    /* Initialize the JS engine -- new/required as of SpiderMonkey 31. */
    if (!JS_Init())
       return 1;

    /* Create a JS runtime. */
    JSRuntime *rt = JS_NewRuntime(8L * 1024L * 1024L, JS_NO_HELPER_THREADS);
    if (rt == NULL)
       return 1;

    /* Create a context. */
    JSContext *cx = JS_NewContext(rt, 8192);
    if (cx == NULL)
       return 1;
    JS_SetOptions(cx, JSOPTION_VAROBJFIX);
    JS_SetErrorReporter(cx, reportError);

    int status = run(cx);

    JS_DestroyContext(cx);
    JS_DestroyRuntime(rt);

    /* Shut down the JS engine. */
    JS_ShutDown();

    return status;
}
每个JSNative有相同的签名,忽视期望js传回的参数。

传给函数的js参数在argc和vp.argc中给出,告诉它回调传了几个参数,JS_ARGV(cx, vp)返回这些参数的一个数组。参数并没有原生的C++类型类似int 和 float;他们是jsval类型。原生的函数使用JS_ConvertArguments来转换参数为C++类型并将他们储存在本地变量中。本地函数使用JS_SET_RVAL(cx, vp, val)来存储它的js返回值。

成功的前提是JSNative必须调用JS_SET_RVAL并且返回JS_TRUE。传给JS_SET_RVAL的值在js回调中返回。

失败的话JSNative调用一个错误报告函数,在这个例子中为JS_ReportError,并且返回JS_FALSE。这样会使异常抛出。调用可以被Try/catch捕捉到。

为了让原生的函数可以被js调用,声明一个JSFunctionSpec表来描述这个函数,然后调用JS_DefineFunctions.

static JSFunctionSpec myjs_global_functions[] = {
    JS_FS("rand",   myjs_rand,   0, 0),
    JS_FS("srand",  myjs_srand,  0, 0),
    JS_FS("system", myjs_system, 1, 0),
    JS_FS_END
};

    ...
    if (!JS_DefineFunctions(cx, global, myjs_global_functions))
        return JS_FALSE;
    ...
一旦函数在golbal中定义了,任何使用golbal的脚本可以调用他们,就像任何的web网页可以调用alert一样。在我们创建的环境中,“hello world”应该这样写:

system("echo hello world");
JSAPI概念

这个版块主要目的是提出JSAPI的重大缺陷,要用spiderMonkey开发必须要读完所有的额版块。

JavaScript values

主要的文章:JS::value

js是一个动态类型的语言:变量和属性没有在编译的时候修正的。那么类似c和C++这些所有变量都有类型的语言如何与js交互呢?JSAPI提供了一个数据类型,JS::Value(也有一个弃用的jsval类型),可以包含任何js值得类型。一个JS::value可以是一个数字、一个字符串和一个bool型的值,一个对象的引用(类似ObjectArrayDate, or Function),或者一个特殊类型例如:null或者undefined。

对于整形和bool的值,jsval包含值,在其他情况下,jsval是一个object, string, or number指针。

jsval包含成员函数类检测js数据的类型。他们是是isObject(),isNumber()isInt32()isDouble()isString()isBoolean()isNull(), and isUndefined().

如果jsval包含一个JSObject,double,或者JSString,你可以把它通过成员函数toObject()toDouble(), and toString()转换成它的基本数据类型。你的应用或者JSAPI函数需要特殊数据类型的值和参数而不是jsval时会很有用。类似的,你可以创建JS::Value绑定一个JSObject、double,JSString指针到javal对象使用JS::ObjectValue(JSObject&),JS::DoubleValue(double)或者JS::StringValue(JSString*).

垃圾回收

当运行时,js 代码隐式分配对象的内存,strings,变量等等。垃圾回收是当有片段的内存不可到达时通过js引擎检查的过程,就是说,他们不能再次使用,并且回收。

垃圾回收有两个重要的影响。第一,应用必须非常确定它需要的任何值都是GC可以到达的。垃圾回收机制对这些非常乐意处理。如果你们有告诉JSAPI你还会用它那么任何你留下的对象将被销毁。第二,应用应该减少垃圾收集的性能影响。

保持对象存在

如果你的JSAPI应用崩溃了,一般都是Gc相关的错误。应用必须确保垃圾回收器可以到达所有仍在使用的对象,数字,字符串。否则,GC将释放被这些值占用的内存,下次使用的时候导致程序崩溃。

下面有几种方法来保证值是GC-reachable。

1、如果你只是需要JSNative调用时限中保持可达,那么将其存储在*rval或者argv数组的一个元素中。储存在这些地方的值总是可达的。要获得更多额外的argx插槽,可以使用 JSFunctionSpec.extra.

2、如果一个自定义的对象需要确定的值保持在内存中,只要将值储存在对象的属性中。对象和属性都将保持可达。如果这些值不需要用js调用,可以使用reserved slots来替代。或者将值储存在私有数据中并实现JSClass.mark。

3、如果一个函数创建了新的对象,string或者数字,它可以使用JS_EnterLocalRootScopeJS_LeaveLocalRootScope来保持这些值在函数的生命周期中存在。

4、要让值永久有效,将他储存在GC root中。

但不管如何,GC bug还是会发生。以下这两个函数(只在DEBUG环境下有效),对调试GC相关的崩溃特为的有用:

1、使用JS_SetGCZeal来开启额外垃圾回收。GC zeal通常会导致GC相关崩溃发生。只用于开发和调试,因为额外的垃圾回收是的js非常慢。

2、使用JS_DumpHeap来转存SpiderMonkey堆或者特殊的感兴趣部分。

GC的性能

过于频繁的垃圾回收会导致性能问题。一些应用可以通过增加JSRuntime的初始化大小来减少垃圾回收的次数。

但它影响到用户时大概最好的技术是在空闲的时间中进行垃圾回收。默认的,js引擎在没有别的选择只有发展过程。这个意味着垃圾回收一般在内存密集型代码运行时发生。应用可以随时通过调用JS_GC or JS_MaybeGC触发垃圾回收。

错误和异常

检查JSAPI函数返回值这一步骤的重要性是没的说的。差不多每个带有JSContext*参数的JSAPI函数都可以出错。系统可能会运行完内存。这里可能会有一个脚本的语法错误。或者脚本显式的抛出一个异常。

js语言和C++都有异常,但是他们不是相同的东西。SpiderMonkey不使用C++异常来处理东西。JSAPI函数从不抛出C++异常,当SpiderMonkey调用一个应用的回调时,回调必须不抛出C++异常。

抛出和捕捉异常

我们已经看了如何从一个JSNative函数中抛出异常。只需要简单调用JS_ReportError,加上printf的格式参数,并且返回JS_FALSE。

  rc = system(cmd);
    if (rc != 0) {
        /* Throw a JavaScript exception. */
        JS_ReportError(cx, "Command failed with exit code %d", rc);
        return JS_FALSE;
    }
这很像JS语句 throw new Error("Command failed with exit code " + rc);。同样注意调用JS_ReportError 不会导致C++异常抛出。它只会创建一个新的js错误对象并且在context中保存作为当前的挂起异常。应用同样返回JS_FALSE。

一旦C++函数返回JS_FALSE,js引擎开始展开js栈,寻找catch或者finally代码块来执行。

错误报告 

自己要做的事自定义错误报告并且把我什么时候报告。

自动处理未捕获的异常  


更多的例子

定义对象和属性

/* 静态初始化一个类来创建“一次性”对象. */
JSClass my_class = {
    "MyClass",

    /* 所有这些都可以用JS_*Stub函数指针来取代. */
    my_addProperty, my_delProperty, my_getProperty, my_setProperty,
    my_enumerate,   my_resolve,     my_convert,     my_finalize
};

JSObject *obj;

/*
 * 定义一个全局范围内的对象可以在for/in循环中枚举。
 * 第二个参数为父对象,三、四参数和普通API调用一样是名字+类。
 * 原型传递是空,所以将使用默认对象原型
 */
obj = JS_DefineObject(cx, globalObj, "myObject", &my_class, NULL,
                      JSPROP_ENUMERATE);

/*
 * 用JSPropertySpec静态数组初始化定义了一堆属性,以{0}结束。除了名字,每个属性还有
 * 一个“tiny”定义(例如MY_COLOR) 可以用在switch语句中(例如下文中的my_getProperty函数)
 */
enum my_tinyid {
    MY_COLOR, MY_HEIGHT, MY_WIDTH, MY_FUNNY, MY_ARRAY, MY_RDONLY
};

static JSPropertySpec my_props[] = {
    {"color",       MY_COLOR,       JSPROP_ENUMERATE},
    {"height",      MY_HEIGHT,      JSPROP_ENUMERATE},
    {"width",       MY_WIDTH,       JSPROP_ENUMERATE},
    {"funny",       MY_FUNNY,       JSPROP_ENUMERATE},
    {"array",       MY_ARRAY,       JSPROP_ENUMERATE},
    {"rdonly",      MY_RDONLY,      JSPROP_READONLY},
    {0}
};

JS_DefineProperties(cx, obj, my_props);

/*
 * 鉴于上述定义和JS_DefineProperties的调用,obj将需要类似这种的“getter”方法在类中。
 */
static JSBool
my_getProperty(JSContext *cx, JSObject *obj, jsval id, jsval *vp)
{
    if (JSVAL_IS_INT(id)) {
        switch (JSVAL_TO_INT(id)) {
          case MY_COLOR:  *vp = . . .; break;
          case MY_HEIGHT: *vp = . . .; break;
          case MY_WIDTH:  *vp = . . .; break;
          case MY_FUNNY:  *vp = . . .; break;
          case MY_ARRAY:  *vp = . . .; break;
          case MY_RDONLY: *vp = . . .; break;
        }
    }
    return JS_TRUE;
}
定义类

通过定义一个构造函数可以将上述的API元素都集合起来:一个原型对象、原型和构造函数的属性都放到一个API调用中。

通过定义类的构造函数、原型、类属性初始化一个类,后者类比于java中的static。他们在构造函数对象的域中定义,所以只有new出来的额class才能使用MyClass.myStaticProp。

JS_InitClass有许多参数,但是你可以对最后四个参数传递NULL如果他们没有这些属性或者方法。

注意你不需要使用JS_InitClass来创建class的新实例,否则创建全局对象时会出现鸡与蛋的问题,但是当你需要一个构造函数通过new来调用时你应该调用JS_InitClass,以新的属性来延伸原型对象(MyClass.Prototype)。总的来说,如果你希望支持多个实例共享行为,使用JS_InitClass。

protoObj = JS_InitClass(cx, globalObj, NULL, &my_class,

                        /* native constructor function and min arg count */
                        MyClass, 0,

                        /* prototype object properties and methods -- these
                           will be "inherited" by all instances through
                           delegation up the instance's prototype link. */
                        my_props, my_methods,

                        /* class constructor properties and methods */
                        my_static_props, my_static_methods);
运行脚本

/* 这里指出源文件的地址. */
char *filename;
uintN lineno;

/*
 * The return value comes back here -- if it could be a GC thing, you must
 * add it to the GC's "root set" with JS_AddRoot(cx, &thing) where thing
 * is a JSString *, JSObject *, or jsdouble *, and remove the root before
 * rval goes out of scope, or when rval is no longer needed.
 */
jsval rval;
JSBool ok;

/*
 * Some example source in a C string.  Larger, non-null-terminated buffers
 * can be used, if you pass the buffer length to JS_EvaluateScript.
 */
char *source = "x * f(y)";

/**
*编译并且执行
*/
ok = JS_EvaluateScript(cx, globalObj, source, strlen(source),
                       filename, lineno, &rval);

if (ok) {
    /* Should get a number back from the example source. */
    jsdouble d;

    ok = JS_ValueToNumber(cx, rval, &d);
    . . .
}

调用函数

/* Call a global function named "foo" that takes no arguments. */
ok = JS_CallFunctionName(cx, globalObj, "foo", 0, 0, &rval);

jsval argv[2];

/* Call a function in obj's scope named "method", passing two arguments. */
argv[0] = . . .;
argv[1] = . . .;
ok = JS_CallFunctionName(cx, obj, "method", 2, argv, &rval);

JSContext

因为维护一个context需要一定的开销,因此应用应该:

1、同一时间只创建需要的context

2、一直保存他们比销毁重建要好得多

如果你的应用创建了多个runtimes,应用可能需要知道那个context对应那个runtime,在这种情况下,使用JS_GetRuntime。

使用JS_SetContextPrivateJS_GetContextPrivate来和context关联应用特定的数据。

初始化内置对象和全局JS对象

对于SpiderMonkey提供的内置对象的完整的列表,参见S_InitStandardClasses.

应用提供给脚本的全局对象决定了脚本能做什么。例如,火狐浏览器使用自己的全局对象:windows。调用JS_SetGlobalObject.来改变全局对象。

创建并初始化自定义对象

除了使用引擎的内置对象,你还可以创建,初始化和使用自己的JS对象。你可以使用JS引擎自动化执行你的应用。自定义JS对象可以提供直接的程序服务,或者他们可以作为你的程序的服务接口。例如,一个自定义的JS对象,他提供的直接服务可能是处理一个应用程序的所有网络访问,或者作为数据库服务的中间件。或者一个数据镜像和函数都已经存在于应用中为C代码提供面向对象的接口或者严格来讲,面向对象本身。这些自定义对象作为应用程序本身的接口,将值从应用程序传递给用户,接受并且在返回给应用之前处理用户输入。这样的一个对象可能被用来提供一个访问基本功能的接口。

有两种方法来创建JS引擎可以使用的自定义对象:

1、编写一个JS脚本创建一个对象,脚本中有他的属性、方法和构造函数,然后再运行时把脚本传递给JS引擎。

2、在你的应用程序中嵌入代码,定义了对象的属性和方法,调用引擎来初始化一个新的对象,然后再通过额外的引擎调用设置对象的属性。这种方法的优点是,你的应用程序可以包含直接操作对象嵌入的本地方法。

在这两种情况下,如果你创建了一个对象,然后希望他一直可谓别的脚本使用的话,你必须要通过调用JS_AddRoot or JS_AddNamedRoot来固定对象。使用这些函数来确保JS引擎会保持对对象的跟踪并且在垃圾回收是清除他们,如果合适的话。、

通过脚本创建对象

通过脚本创建对象的其中一个原因在于当你只需要一个对象,它只在脚本运行过程中存在。要创建一个之持久保存于脚本调用的对象,你可以用嵌入对象代码来替代。

注意:你也可以通过脚本来创建持久保存的对象。

使用脚本创建自定义对象:

1、定义并且设定对象的细则。

2、编写定义脚本代码并且创建对象。例如: function myfun(){ var x = newObject(); . . . } 注意:  在你的应用中嵌入相应的JS引擎调用来编译并且运行脚本。你有两个选择:1、编译并且执行脚本使用一个简单的JS_EvaluateScript调用,JS_EvaluateUCScript 。2、先使用JS_CompileScript或者 JS_CompileUCScript编译脚本,然后使用JS_ExecuteScript各自执行。“UC”版本为为脚本提供Unicode编码支持。

使用脚本创建的一个对象只能在脚本的生命周期里可使用,或者可以创建持久型脚本。通常情况下,一旦脚本执行完成了,他的独显被销毁了。在很多情况下,这个行为正是你所需要的。但在有些时候,你可能想让对象在你的应用生命周期里持久化。在这些情况下你需要直接在你的应用程序中嵌入你的对象创建代码,或者你需要直接把对象关联到全局对象上,这样只要全局对象存在,它就存在。

自定义对象

 应用程序可以在不需要JSClass支持下创建对象。

1、用C或C++实现自定义对象的getter、setter和方法。为每个getter和setter编写一个JSPropertyOp。为每个方法编写一个JSNative或者JSFastNative。

2、声明一个包含自定义对象的属性规格的JSFunctionSpec数组,包括getter和 setter。

3、声明一个包含自定义对象的方法规格的JSFunctionSpec数组。

4、调用JS_NewObjectJS_ConstructObject, or JS_DefineObject来创建对象。

5、调用 JS_DefineProperties来定义对象的属性。

6、调用JS_DefineFunctions来定义对象的方法。

JS_SetProperty也可以用在创建对象的属性。这个属性创建是没有getter和setter的,是普通的js属性。

为对象提供私有数据

类似contexts,你可以与一个对象关联大量的数据而不需要储存数据到对象本身。调用JS_SetPrivate 指定要建立私有数据的对象,并指定指向数据的指针。

例如:

 JS_SetPrivate(cx, obj, pdata);
在之后要获取数据,可以调用 JS_GetPrivate,并且作为一个参数传递对象。这个函数返回对象私有数据的指针:

 pdata = JS_GetPrivate(cx, obj);
编译脚本

允许脚本最简单的方法就是使用JS_EvaluateScript,它将编译和运行绑定到一块去了。

但是有时候一个应用需要多次运行一个脚本。在这种情况下,编译一次执行多次要快一点。

JSAPI提供JSScript类型来代表一个编译好的脚本。JSScript的生命周期如下:

应用程序使用JS_CompileScriptJS_CompileUTF8File, orJS_CompileFileHandle编译一些js代码。这些函数返回一个新的JSScript的指针。

应用调用JS_ExecuteScript (or JS_ExecuteScriptPart) 任意次数。在不同的contexts和不同的global对象中使用JSScript是安全的,但必须保证在创建时的runtime和线程中。

下面是一个例子:

/*
 * Compile a script and execute it repeatedly until an
 * error occurs.  (If this ever returns, it returns false.
 * If there's no error it just keeps going.)
 */
JSBool compileAndRepeat(JSContext *cx, const char *filename)
{
    JSScript *script;

    script = JS_CompileUTF8File(cx, JS_GetGlobalObject(cx), filename);
    if (script == NULL)
        return JS_FALSE;   /* compilation error */

    for (;;) {
        jsval result;

        if (!JS_ExecuteScript(cx, JS_GetGlobalObject(cx), script, &result))
            break;
        JS_MaybeGC(cx);
    }

    return JS_FALSE;
}
已编译脚本的生命周期是和js对象的生命周期关联的,当脚本不再可达的时候垃圾回收器销毁脚本。JSAPI通过 JS_NewScriptObject函数来提供这一特点,使用这一特点的脚本的生命周期是这样的:

1、应用编译了一些js代码

2、为了保护已编译的脚本免受垃圾回收器的误删,应用通过调用JS_NewScriptObject创建了一个已编译对象并且使用JS_SetPropertyJS_SetReservedSlotJS_AddRoot或其他函数来使得这个对象是垃圾回收可达的。

3、应用执行任意次数的已编译脚本。

4、在应用的执行中,当已编译脚本终于再也不需要了,已编译脚本将变成不可达的。

5、最终垃圾回收器回收不可达的脚本和它的其他部分。

下面是一个例子示范这个技术--注意在这个例子中不够复杂去保证JS_NewScriptObject的使用,上面的例子更加直接。

/*
 * Compile a script and execute it repeatedly until an
 * error occurs.  (If this ever returns, it returns false.
 * If there's no error it just keeps going.)
 */
JSBool compileAndRepeat(JSContext *cx, const char *filename)
{
    JSScript *script;
    JSObject *scriptObj;

    script = JS_CompileUTF8File(cx, JS_GetGlobalObject(cx), filename);
    if (script == NULL)
        return JS_FALSE;   /* compilation error */

    scriptObj = JS_NewScriptObject(cx, script);
    if (scriptObj == NULL) {
        JS_DestroyScript(cx, script);
        return JS_FALSE;
    }

    if (!JS_AddNamedObjectRoot(cx, &scriptObj, "compileAndRepeat script object"))
        return JS_FALSE;

    for (;;) {
        jsval result;

        if (!JS_ExecuteScript(cx, JS_GetGlobalObject(cx), script, &result))
            break;
        JS_MaybeGC(cx);
    }

    JS_RemoveObjectRoot(cx, &scriptObj);  /* scriptObj becomes unreachable
                                             and will eventually be collected. */
    return JS_FALSE;
}
跟踪分析

JSAPI提供了可以便捷的实现js跟踪和分析的功能。

函数跟踪

如果你配置了启用js调用跟踪,你可以使用JS_SetFunctionCallback()来建立一个C函数,它会在js函数将要调用或者执行完成之后调用:

void funcTransition(const JSFunction *func,
                    const JSScript *scr,
                    const JSContext *const_cx,
                    JSBool entering)
{
  JSContext *cx = const_cast<JSContext*>(const_cx);
  JSString *name = JS_GetFunctionId((JSFunction*)func);
  const char *entExit;
  const char *nameStr;

  /* build a C string for the function's name */

  if (!name) {
    nameStr = "Unnamed function";
  } else {
    nameStr = JS_EncodeString(cx, name);
  }

  /* build a string for whether we're entering or exiting */

  if (entering) {
    entExit = "Entering";
  } else {
    entExit = "Exiting";
  }

  /* output information about the trace */

  printf("%s JavaScript function: %s at time: %ld", entExit, nameStr, clock());
} 

void enableTracing(JSContext *cx) {
  JS_SetFunctionCallback(cx, funcTransition);
}

void disableTracing(JSContext *cx) {
  JS_SetFunctionCallback(cx, NULL);
}

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值