NPAPI插件开发学习:NPAPI和NPRuntime的介绍

本篇文章在探讨 NPAPI NPRuntime的设计,并非 Plugin教学。

当时因为看到公司内部写出来的 Plugin问题不少,而且网络上说明太少,特地写来给大家看的~

故本篇没有详细介绍每个 API的使用与功能,请见谅啰!

This article was written in2009/04/08.

 

NPAPI &NPRuntime 简介

Netscape PluginApplication Programming Interface (NPAPI)

NPAPI 原本是由 Netscape所制定的一组单纯的 C Plugin API,起初是无法支持 Scriptability;于是到了 2004 年底时,各家 Browser (IE, Opera, Mozilla) 都同意支持 NPRuntime延伸 API 以支持 Scriptability,所以目前若是想写 Plugin则应该以 NPRuntime API 才能跨不同的 Browsers

 

Plugin LifeCycle

(转载的时候修改了图片中的部分内容,如图所示)

上面的 Sequence Diagram说明了 Browser Plugin 之间的运作过程:

1.   Browser lookup Plugin (.so, .dll) and load it.

2.   Browser呼叫 Plugin NP_Initialize() 来交换彼此所需的 API FunctionPointers

o    Browser Side NPN_API function table(NPNetscapeFuncs *aNPNFuncs)传给 Plugin(Binding)

o   Plugin应将其自身所定义好的 NPP API functions填入 NPPluginFuncs*aNPPFuncs中,好让 Browser得到 Plugin Side Function pointers

3.   Browser呼叫 Plugin NP_GetValue() 来得到 Plugin 的信息,例如:版本信息与是否支持 Scriptability等。

4.   Browser在网页中发现 Plugin所支持的 Mime Type时,呼叫 Plugin NPP_New()来建立新的 Plugin instance来处理。

5.   当网页被 Unload前,Browser 则会呼叫 PluginNPP_Destroy()来通知 Plugin Destroy 所对应的 Plugin instance

6.    Browser程序结束前会呼叫 Plugin NP_Shutdown() Destruction,结束整个 Plugin LifeCycle 

 

以下为  API宣告:

char*          NP_GetMIMEDescription() // Unix only
NPError        NP_GetValue(void*,NPPVariable, void* out)
NPError       NP_Initialize(NPNetscapeFuncs*, NPPluginFuncs*)
NPError OSCALL NP_Shutdown()

 

NPNetscapeFuncs(NPN_XXXXX API)

NPNetscapeFuncs 是一个 Functionpointer table,是 Browser传给 Plugin 使用的 NPN_XXXXX API

宣告如下:

typedef struct _NPNetscapeFuncs {
    uint16 size;
    uint16 version;

    NPN_GetURLProcPtr geturl;
    NPN_PostURLProcPtr posturl;
    NPN_RequestReadProcPtr requestread;
    NPN_NewStreamProcPtr newstream;
    NPN_WriteProcPtr write;
    NPN_DestroyStreamProcPtrdestroystream;
    NPN_StatusProcPtr status;
    NPN_UserAgentProcPtr uagent;
    NPN_MemAllocProcPtr memalloc;
    NPN_MemFreeProcPtr memfree;
    NPN_MemFlushProcPtr memflush;
    NPN_ReloadPluginsProcPtrreloadplugins;
    NPN_GetJavaEnvProcPtr getJavaEnv;
    NPN_GetJavaPeerProcPtr getJavaPeer;
    NPN_GetURLNotifyProcPtr geturlnotify;
    NPN_PostURLNotifyProcPtrposturlnotify;
    NPN_GetValueProcPtr getvalue;
    NPN_SetValueProcPtr setvalue;
    NPN_InvalidateRectProcPtrinvalidaterect;
    NPN_InvalidateRegionProcPtrinvalidateregion;
    NPN_ForceRedrawProcPtr forceredraw;

    NPN_GetStringIdentifierProcPtrgetstringidentifier;
    NPN_GetStringIdentifiersProcPtrgetstringidentifiers;
    NPN_GetIntIdentifierProcPtrgetintidentifier;
    NPN_IdentifierIsStringProcPtridentifierisstring;
    NPN_UTF8FromIdentifierProcPtr utf8fromidentifier;
    NPN_IntFromIdentifierProcPtrintfromidentifier;
    NPN_CreateObjectProcPtr createobject;
    NPN_RetainObjectProcPtr retainobject;
    NPN_ReleaseObjectProcPtrreleaseobject;
    NPN_InvokeProcPtr invoke;
    NPN_InvokeDefaultProcPtr invokeDefault;
    NPN_EvaluateProcPtr evaluate;
    NPN_GetPropertyProcPtr getproperty;
    NPN_SetPropertyProcPtr setproperty;
    NPN_RemovePropertyProcPtrremoveproperty;
    NPN_HasPropertyProcPtr hasproperty;
    NPN_HasMethodProcPtr hasmethod;
    NPN_ReleaseVariantValueProcPtrreleasevariantvalue;
    NPN_SetExceptionProcPtr setexception;
    NPN_PushPopupsEnabledStateProcPtrpushpopupsenabledstate;
    NPN_PopPopupsEnabledStateProcPtrpoppopupsenabledstate;
    NPN_EnumerateProcPtr enumerate;
    NPN_PluginThreadAsyncCallProcPtrpluginthreadasynccall;
    NPN_ConstructProcPtr construct;
    NPN_ScheduleTimerProcPtrscheduletimer;
    NPN_UnscheduleTimerProcPtrunscheduletimer;
    NPN_PopUpContextMenuProcPtrpopupcontextmenu;
} NPNetscapeFuncs;

 

NPPluginFuncs(NPP_XXXX API)

NPPluginFuncs 也是一个 Functionpointer table,是由 Plugin传回给 Browser使用的 NPP_XXXXX API

宣告如下:

typedef struct _NPPluginFuncs {
    uint16 size;
    uint16 version;
    NPP_NewProcPtr newp;
    NPP_DestroyProcPtr destroy;
    NPP_SetWindowProcPtr setwindow;
    NPP_NewStreamProcPtr newstream;
    NPP_DestroyStreamProcPtrdestroystream;
    NPP_StreamAsFileProcPtr asfile;
    NPP_WriteReadyProcPtr writeready;
    NPP_WriteProcPtr write;
    NPP_PrintProcPtr print;
    NPP_HandleEventProcPtr event;
    NPP_URLNotifyProcPtr urlnotify;
    JRIGlobalRef javaClass;
    NPP_GetValueProcPtr getvalue;
    NPP_SetValueProcPtr setvalue;
} NPPluginFuncs;

 

Plugin InstanceConstruction and Destruction

Browser HTML 中发现 Plugin 所对应的 Mime Type 时,会呼叫 NPP_New() 来向 Plugin 要求一个 Plugin Instace服务。

NPP_New() 定义如下:

#include <npapi.h>

NPError NPP_New(NPMIMEType pluginType,
                NPP instance,
                uint16 mode,
                int16 argc,
                char* argn[],
                char* argv[],
                NPSavedData* saved);

NPError NPP_Destroy(NPP instance,
                    NPSavedData **save);

NPP 即为 Plugin Instance数据结构,由 Browser所建立,透过 NPP_New()传送给 Plugin

NPP 数据结构很简单,仅包含两个 void pointer

1.   void *pdata : Plugin Private Data

2.   void *ndata : Browser Private Data

宣告如下:

/*
 * NPP is a plug-in's opaque instance handle
 */
typedef struct _NPP
{
    void*    pdata;            /* plug-in private data */
    void*    ndata;            /* netscape private data */
} NPP_t;
typedef NPP_t*    NPP;

Browser在某个 Page Unload 之前,则会呼叫 NPP_Destroy()来通知 Plugin 结束所对应的 Plugin Instance

Scriptability

Scriptability 就是让 JavaScript可以将 Plugin 当作 JavaScriptObject来使用,而 NPRuntime定义了 NPObject NPClass 两个结构来建立 Browser 能够了解的 Scriptable Object

MultipleNPObject Instances

该注意的一点是,NPObject本身也是需要支持 MultipleInstance,原因很简单,因为 Plugin Instance都应该拥有自己的 NPObject,若是 NPObject不设计成 Multiple Instance,就得所有 Plugin Instance「共享」一组 NPObject,将会带来很多扩充性上的困难。

 

ScriptableObject Model (NPObject & NPClass design with UML)

以下是个人从 NPRuntime设计中理解出的 Scriptable Object Model (名字取不好,多见谅。)

 

What is NPClass?

NPClass 是一组 Interface(function pointer table),代表某个 NPObject在建立 Instance时所需要的动作(ex:Constructor/Desctructor),也就是说 Browser只透过 NPClass所指定的 Methods来建立新的 NPObject Instance。举例来说,当 Browser透过 NPP_GetValue()来向 Plugin 要一个 Property 时,Plugin 可以传回一个 NPObject Browser ,让 Browser 知道其实这个 Plugin Property其实是一个 ScriptableObject

NPClass 其实就是所谓的 Marshaling Functions,这个原本由 RPC 发展出来的方法已经在很多地方都可以看到,几乎只要是 Virtual Machine相关的系统都会用这个方式来达到模拟 Calling Convention的目的。
不过也有例外的,像是 Mozilla XPCOM xptcall 就是直接从 register/stack 来做 Marshaling的动作。

PluginObject 是我们实际上想建立的 Object,它应该具有我们想要的 Custome Properties Methods,而 PluginObject所拥有的 NPClass其实就是 PluginClass,也是由我们设计的,因为只有设计者才知道 Plugin Object应该如何 construct/destruct/etc…)

Browser需要建立 NPObjectInstance时,会呼叫 NPObjectNPClassallocate(),也就会呼叫到 PluginClasspluginAllocate(),我们就可以 newPluginObject()传回给 Browser了。简单的说,Browser想要建立或是存取任何 PluginObject,都得透过 PluginClass中的 APIBrowser是无法直接存取 PluginObject CustomProperty/Methods

虽然 NPRuntime的呼叫都是 C API,但是实际上这组 API 想完成的事就是上面的 Scriptable Object Model。若只是了解 Call Flow是不够的,要能「读出」原来设计这组 API 的人在「想」的是什么。

NPClass NPObject C 宣告如下:

struct NPClass
{
    uint32_t structVersion;
    NPAllocateFunctionPtr allocate;
    NPDeallocateFunctionPtr deallocate;
    NPInvalidateFunctionPtr invalidate;
    NPHasMethodFunctionPtr hasMethod;
    NPInvokeFunctionPtr invoke;
    NPInvokeDefaultFunctionPtrinvokeDefault;
    NPHasPropertyFunctionPtr hasProperty;
    NPGetPropertyFunctionPtr getProperty;
    NPSetPropertyFunctionPtr setProperty;
    NPRemovePropertyFunctionPtrremoveProperty;
    NPEnumerationFunctionPtr enumerate;
};

struct NPObject {
    NPClass *_class;
    uint32_t referenceCount;
    // Additional space may be allocatedhere by types of NPObjects
}

 

When shouldNPObject be created?

NPP_New()要求建立 Plugin Instance,就需要建立我们的PluginObject(which is a NPObject) Instance,在此同时,应该直接以 NPN_CreateObject()来建立相对应的 NPObject,因为 NPObject是由 Browser 来主动 Allocate/Free Memory,所以 reference count也会纪录在 NPObject中。

以下为使用 NPN_CreateObject() 来建立 NPObject 的范例:

#include <npruntime.h>

NPObject *NPN_CreateObject(NPP npp, NPClass *aClass);

NPError NPP_New(NPMIMEType pluginType,
                NPP instance,
                uint16 mode,
                int16 argc,
                char* argn[],
                char* argv[],
                NPSavedData* saved)
{
    // Scripting functions appeared inNPAPI version 14
    if (browser->version >= 14)
        instance->pdata =NPN_CreateObject ((NPP) instance,
                                           (NP_Class *) PluginClass);
}

NPN_CreateObject() 是由 Plugin Browser 要求建立一个 NPObject,此 NPObject NPClass 就是我们所指定的 PluginClass

若是 PluginClass中有指定 allocate(),则 Browser会使用 PluginClass -> allocate()来做来替 NPObject allocate Memory,否则 Browser会以 malloc() Allocate NPObject。传回的 NPObject reference count会变成 1

这里有一件很重要的技巧,PluginClass中的 pluginAllocate()应该要 newPluginObject传回给 Browser,因为 PluginObject「继承自」 NPObject Browser的角度来看,PluginObject就只是个 NPObject;但是对于 Plugin来说,instancepdata所存放的其实是 PluginObject;之后 Browser呼叫 PluginClass中的 Methods 时,我们只需要取得 instancepdata就可以当做是 PluginObject,并直接存由 Plugin自己定义的 CustomProperties/Methods了。如此一来,就不用一堆 Variable指来指去的了,这是简化支持 MultipleInstance的一个重点。

这样的技巧在 AppleObjective-C Object-Oriented Language (ex: C++vtable)里是很常见的,可惜的是我们目前的实作完全没有 OO的思考。
instance->pdata
反正是个 void *,可以任意 casting成任一种 typeBrowser Plugin 就像一个中国各自表述啦~

 

Browser ask forNPObject

Browser NPP_New()建立 Plugin Instance之后,还会以

 NPP_GetValue(npp,NPPVpluginScriptableNPObject, void* value);

来询问 Plugin是否支持 Scriptable,此时再把我们先前建立好的 PluginObject (stored in instance->pdata)透过 value 传回给 Browser 即可。

范立如下:

NPError NPP_GetValue(NPP instance,NPPVariable variable, void *value)
{
    if (variable ==NPPVpluginScriptableNPObject) {
        void **v = (void **)value;
        PluginObject *obj = instance->pdata;

        if (obj)
           NPN_RetainObject((NPObject*)obj);

        *v = obj;
        return NPERR_NO_ERROR;
    }
    return NPERR_GENERIC_ERROR;
}

NPRuntime API 中规定,在传回 NPObject之前,应该先以 NPN_RetainObject()来增加 NPObject.refCount,这对 JavaScriptEngine Garbage Collection机制很重要,千万别忘了。

 

How NPObject wasused by JavaScript?

 

2 Types ofMarshaling Functions (Method/Property)

在上面建立完 Scriptable NPObject后,等于是建立了一个相对应的 JavaScript Object。于是 JavaScript可以对 JavaScript Object做其它的存取动作,而这些动作则被对应到 NPObject的两类 Marshaling Functions

(提醒一下:NPObject->_class就是 NPClass )

1.   Method呼叫

typedef bool(*NPHasMethodFunctionPtr)(NPObject *obj, NPIdentifier name);
typedef bool (*NPInvokeFunctionPtr)(NPObject *obj, NPIdentifier name, constNPVariant *args, uint32_t argCount, NPVariant *result);
typedef bool (*NPInvokeDefaultFunctionPtr)(NPObject *npobj, const NPVariant*args, uint32_t argCount, NPVariant *result);

struct NPClass
{
    ...
    NPHasMethodFunctionPtr hasMethod;
    NPInvokeFunctionPtr invoke;
    NPInvokeDefaultFunctionPtr invokeDefault;
    ...
};

2.   Property存取

typedef bool(*NPHasPropertyFunctionPtr)(NPObject *obj, NPIdentifier name);
typedef bool (*NPGetPropertyFunctionPtr)(NPObject *obj, NPIdentifier name,NPVariant *result);
typedef bool (*NPSetPropertyFunctionPtr)(NPObject *obj, NPIdentifier name,const NPVariant *value);
typedef bool (*NPRemovePropertyFunctionPtr)(NPObject *npobj, NPIdentifier name);

struct NPClass
{
    ...
    NPHasPropertyFunctionPtr hasProperty;
    NPGetPropertyFunctionPtr getProperty;
    NPSetPropertyFunctionPtr setProperty;
    NPRemovePropertyFunctionPtr removeProperty;
    ...
};

从以下范例说明会比较清楚:

<script>
var myPlugin = document.getElementByID("FooPlugin");
myPlugin.fooMethod();
myPlugin.fooProperty = "hello world";
</script>

ECMAScript的角度说明如下:

·        myPlugin : "myPlugin"是一个 JavaScript Object的名字(Identifier),之后可以将 myPlugin想象成 NPObject(实际上 JavaScript VM内部的对应要复杂许多)

·        fooMethod : "fooMethod"是我们 Plugin 所提供的 Method 的名字(Identifier),则 myPlugin.fooMethod();会转换成对 NPObject NPClass function call ,动作如下:
1.
透过 hasMethod(NPObject, NPIdentifier of"fooMethod")询问 Plugin 是否提供名称为 "fooMethod" Method,若有则到 2.
2.
透过 invokeMethod(NPObject, NPIdentifier of"fooMethod", …)来传送参数给 Plugin,而 Plugin则可由 NPIdentifier得知 Browser 希望呼叫的 method 为何,再去执行所对应的功能,最后再传回值 (result)

·        fooProperty : "fooProperty"是我们 Plugin 所提供的 Property 的名字(Identifier),则 myPlugin.fooProperty= "hello world";会转换成为以下动作:
1.
透过 hasProperty(NPObject, NPIdentifier of"fooProperty");询问 Plugin 是否有提供名称为 "fooProperty" Property,若有则到 2.
2.
透过 setProperty(NPObject, NPIdentifier of"fooProperty", "hello world");要求 Plugin 执行将 fooProperty 的值更改为 "hello world"的动作。

以上说明着动在流程上,细节上并非完全正确,因为 JavaScript(ECMAScript)内部有许多针对 Objects, Properties, Attributes等细节,可以说上三天三夜了吧!

 

NPVariant(Parameters Serialization between JavaScript and C)

Marshaling Functions中,会以 NPVariant来传送真正的参数数据。

NPVariant 就是参数的 Serialized DataType

typedef struct _NPVariant {
    NPVariantType type;
    union {
        bool boolValue;
        int32_t intValue;
        double doubleValue;
        NPString stringValue;
        NPObject *objectValue;
    } value;
} NPVariant;

typedef enum {
    NPVariantType_Void,
    NPVariantType_Null,
    NPVariantType_Bool,
    NPVariantType_Int32,
    NPVariantType_Double,
    NPVariantType_String,
    NPVariantType_Object
} NPVariantType;

 

Data TypeMapping Between JavaScript and NPVariant

NPVariant 所封装的数据型态会对应到 JavaScript数据型态。

对应如下:

JavaScript

C (NPVariant with type:)

undefined

NPVariantType_Void

null

NPVariantType_Null

Boolean

NPVariantType_Bool

Number

NPVariantType_Double or NPVariantType_Int32

String

NPVariantType_String

Object

NPVariantType_Object

 

Marshaling Macro

为了方便 NPVariant JavaScript间的数据转换, NPRuntime也定义了一组转换的 Macro方便程序设计。

#define NPVARIANT_IS_VOID(_v)    ((_v).type == NPVariantType_Void)
#define NPVARIANT_IS_NULL(_v)   ((_v).type == NPVariantType_Null)
#define NPVARIANT_IS_BOOLEAN(_v) ((_v).type == NPVariantType_Bool)
#define NPVARIANT_IS_INT32(_v)  ((_v).type == NPVariantType_Int32)
#define NPVARIANT_IS_DOUBLE(_v) ((_v).type == NPVariantType_Double)
#define NPVARIANT_IS_STRING(_v) ((_v).type == NPVariantType_String)
#define NPVARIANT_IS_OBJECT(_v)  ((_v).type== NPVariantType_Object)

#define NPVARIANT_TO_BOOLEAN(_v) ((_v).value.boolValue)
#define NPVARIANT_TO_INT32(_v)  ((_v).value.intValue)
#define NPVARIANT_TO_DOUBLE(_v) ((_v).value.doubleValue)
#define NPVARIANT_TO_STRING(_v)  ((_v).value.stringValue)
#define NPVARIANT_TO_OBJECT(_v) ((_v).value.objectValue)

#define NP_BEGIN_MACRO  do {
#define NP_END_MACRO    } while (0)

#define VOID_TO_NPVARIANT(_v)               NP_BEGIN_MACRO (_v).type = NPVariantType_Void; (_v).value.objectValue =NULL; NP_END_MACRO
#define NULL_TO_NPVARIANT(_v)               NP_BEGIN_MACRO (_v).type = NPVariantType_Null; (_v).value.objectValue =NULL; NP_END_MACRO
#define BOOLEAN_TO_NPVARIANT(_val, _v)      NP_BEGIN_MACRO (_v).type = NPVariantType_Bool; (_v).value.boolValue =!!(_val); NP_END_MACRO
#define INT32_TO_NPVARIANT(_val, _v)        NP_BEGIN_MACRO (_v).type = NPVariantType_Int32; (_v).value.intValue =_val; NP_END_MACRO
#define DOUBLE_TO_NPVARIANT(_val, _v)       NP_BEGIN_MACRO (_v).type = NPVariantType_Double; (_v).value.doubleValue= _val; NP_END_MACRO
#define STRINGZ_TO_NPVARIANT(_val, _v)      NP_BEGIN_MACRO (_v).type = NPVariantType_String; NPString str = { _val,strlen(_val) }; (_v).value.stringValue = str; NP_END_MACRO
#define STRINGN_TO_NPVARIANT(_val, _len, _v) NP_BEGIN_MACRO (_v).type =NPVariantType_String; NPString str = { _val, _len }; (_v).value.stringValue =str; NP_END_MACRO
#define OBJECT_TO_NPVARIANT(_val, _v)       NP_BEGIN_MACRO (_v).type = NPVariantType_Object; (_v).value.objectValue= _val; NP_END_MACRO

 

Why needNPVariant (Serialization) ?

JavaScript中,使用者可以任意撰写任何 function,而这些 function的参数个数,型态,排列顺序等,都是任意的;我们不可能写出一个 C function来对应到所有的 JavaScript functionC function compile time时就必须决定参数个数,型态与顺序;因此必须要透过 Marshaling(Serialization) 的方式来取得 JavaScript的参数后,再转换成 C语言中相对应的数据型态来处理。

NPIdentifier

NPObject Method Property 的皆是由 NPIdentifier 来指定,NPIdentifier 对于相同名称的 Method 或是 Property 会有一个 Unique 值。而 NPIdentifier 的值是由 Browser 所提供,也就是说 Browser 内部有一个 (Hash) Table来储存所有的 NPIdentifier

typedef void *NPIdentifier;

/*
    NPObjects have methods and properties.  Methods and properties are
    identified with NPIdentifiers.  These identifiers may be reflected
    in script.  NPIdentifiers can be either strings orintegers, IOW,
    methods and properties can beidentified by either strings or
    integers (i.e. foo["bar"]vs foo[1]). NPIdentifiers can be
    compared using ==.  In case of any errors, the requested
    NPIdentifier(s) will be NULL.
*/
NPIdentifier NPN_GetStringIdentifier(const NPUTF8 *name);
void NPN_GetStringIdentifiers(const NPUTF8 **names, int32_t nameCount,
                             NPIdentifier *identifiers);
NPIdentifier NPN_GetIntIdentifier(int32_t intid);
bool NPN_IdentifierIsString(NPIdentifier identifier);

/*
    The NPUTF8 returned fromNPN_UTF8FromIdentifier SHOULD be freed.
*/
NPUTF8 *NPN_UTF8FromIdentifier(NPIdentifier identifier);

/*
    Get the integer represented byidentifier. If identifier is not an
    integer identifier, the behaviour isundefined.
*/
int32_t NPN_IntFromIdentifier(NPIdentifier identifier);

Browser 另外还提供了 NPIdentifier的转换函数,供 Plugin方便使用。

Why useNPIdentifier?

NPIdentifier这样的设计主要有两个原因:

1.   Less Memory Cost
对于许多 Object来说都有相同名称的 Method或是 Property,若是将这些「名称字符串」全都储存在 Object Instance 中,对于 Memory 的消耗实在是一种浪费。

2.   Fast Lookup (ECMAScript Identifer Resolution)
对于 Browser 或是 Plugin JavaScript执行时在 Lookup Object的动作时,能够以 Lookup NPIdentifier来取代 Name StringCompare,可以大大增加 Lookup的速度。

 

Reference

·        https://developer.mozilla.org/en/Plugins

·        The NPAPI Plugin Guide

 

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值