node-sqlite3源码分析 - Database::run方法
以下是lib/sqlite3.js文件内的一段: Database.prototype.run = function(sql) { var params = Array.prototype.slice.call(arguments,1); var statement = new Statement(this,sql,errorCallback(params)); statement.run.apply(statement,params).finalize(); return this; }; 之所以找到这段,是因为主页https://github.com/developmentseed/node-sqlite3介绍使用方法时用到了Database的run方法。示例的目的是创建一张表。 db.run("CREATE TABLE lorem (info TEXT)"); 可以看出Database的run方法不是在node的C++ addon中实现的,而是在javascript文件中实现的。Database的run方法内部创建了一个addon中实现的Statement对象。创建Statement时将Database实例传入构建函数,第二个参数是SQL语句,第三个参数是回调函数。创建Statement C++对象时,就会生成一个PrepareBaton用来实现调用sqlite3_prepare_v2,目的就是为待执行的sql语句生成一个内部结构。接着Statement的run方法又会生成另一个RunBaton用来实现调用sqlite3_step,目的就是执行准备好的sql语句。
Array.prototype.slice.call 所有的javascript函数都有属于自己的一个arguments对象。这条javascript语句应该是说将Database的run函数的实参转换成数组params。但不是转换run函数的所有实参,只需要从索引位置1到最后一个参数。即,排除第一个参数。但明明这里只有一个参数。那是不是说这里只是想创建一个空数组。忘了看这个函数前的一句注释: // Database#run(sql,[bind1,bind2,...],[callback]) 这句注释是说run函数的使用方法。这与函数说明有冲突,函数声明只有一个参数,就是sql。也许javascript语言可以允许可变个数参数列表。注释明显是在说明,除了sql参数外其他参数都是可选的。 new Statement // { Database db,String sql,Array params,Function callback } Handle<Value> Statement::New(const Arguments& args) { HandleScope scope; if (!args.IsConstructCall()) { return ThrowException(Exception::TypeError( String::New("Use the new operator to create new Statement objects")) ); } int length = args.Length(); if (length <= 0 || !Database::HasInstance(args[0])) { return ThrowException(Exception::TypeError( String::New("Database object expected"))); } else if (length <= 1 || !args[1]->IsString()) { return ThrowException(Exception::TypeError( String::New("SQL query expected"))); } else if (length > 2 && !args[2]->IsUndefined() && !args[2]->IsFunction()) { return ThrowException(Exception::TypeError( String::New("Callback expected"))); }//这之上的语句都是检查创建Statement时传入的参数是否足够以及是否合法。 Database* db = ObjectWrap::Unwrap<Database>(args[0]->ToObject());//将第一个参数转换成Database类实例。 Local<String> sql = Local<String>::Cast(args[1]);//将第二个参数转换成Local<String>类型的变量实例。 args.This()->Set(String::NewSymbol("sql"),ReadOnly); Statement* stmt = new Statement(db);//创建Statement C++对象。 stmt->Wrap(args.This()); PrepareBaton* baton = new PrepareBaton(db,Local<Function>::Cast(args[2]),stmt);//创建PrepareBaton对象,提供Database、Statement和javascript下的回调函数。javascript下的回调函数需转换成Local<Function>类型。 baton->sql = std::string(*String::Utf8Value(sql)); db->Schedule(Work_BeginPrepare,baton); return args.This(); } 待会儿再详细分析New函数内部的实现细节。先看看为何在javascript下执行new Statement这句会映射到C++中Statement类的New函数。 建立javascript new与addon中函数对应关系 这与Node addon开发规范有关。在任何一个node的addon源码中必须出现这句: NODE_MODULE(node_sqlite3,RegisterModule); NODE_MODULE是个宏,作用是申明一个可供外部调用的一个函数。addon在windows平台下就是一个dll。宏的作用就是向外公布一个函数。这个函数是个通用函数,它的名称固定不变,由nodejs负责调用用来初始化addon。记住,宏的第一个参数必须与adoon,或者说dll的文件名一致。我摘录的这句出现在node_sqlite3的node_sqlite3.cc源代码文件中。RegisterModule函数也在这个文件内。在这个函数的最前部看到有如下两条语句: void RegisterModule(v8::Handle<Object> target) { Database::Init(target); Statement::Init(target); 调用了Datebase和Statement的Init函数。看样子这两个Init函数是静态函数。查看Database.h和Statement.h,这两个函数确实是静态函数。无论是Database还是Statement的init函数内都出现了这几条语句: Local<FunctionTemplate> t = FunctionTemplate::New(New); constructor_template = Persistent<FunctionTemplate>::New(t); constructor_template->InstanceTemplate()->SetInternalFieldCount(1); constructor_template->SetClassName(String::NewSymbol("Statement")); FunctionTemplate::New(New),括号内的New是指Database或者Statement的New静态函数。constructor_template也是Database或者Statement的New成员变量。我认为这四条语句的作用就是告诉nodejs,如果javascript中出现new Statement时,请调用Statement的New静态函数。同样也适用Database类。 Database* db = ObjectWrap::Unwrap<Database>(args[0]->ToObject()); ObjectWrap::Unwrap应该是个模板函数,它可以返回Database指针。还有一点需要注意,Unwrap的参数是args[0]->ToObject()。可以理解为,V8将javascript提供的所有参数封装成一个对象Arguments供C++使用。可以通过Arguments访问所有的参数。看到这,我突然想到之前看过的一篇文章里面介绍了一些V8的基本概念,对于理解V8相关的代码有些帮助,http://www.ibm.com/developerworks/cn/opensource/os-cn-v8engine/index.html。 接着是转换第二个参数:sql语句。有一点不明白,为何不直接将其转成std::string,或者c-string,而是转成V8下的Local<String>。 Local<String> sql = Local<String>::Cast(args[1]); 刚才提到的那篇文章写到:“在 V8 中,handle 分为两种:持久化 (Persistent)handle 和本地 (Local)handle,持久化 handle 存放在堆上,而本地 handle 存放在栈上。这个与 C/C++ 中的堆和栈的意义相同 ( 简而言之,堆上的空间需要开发人员自己申请,使用完成之后显式的释放;而栈上的为自动变量,在退出函数 / 方法之后自动被释放 )。”。很明显,这里是本地handle,会自动释放资源。但也不能说明一定得使用Local<String>类型,因为创建std::string对象也是在栈上同样可以自动释放。接下来的一句也许能说明问题。 args.This()->Set(String::NewSymbol("sql"),ReadOnly); 现在只能这么理解,因为Set函数需要一个Local<String>类型的变量。这句的意图是什么不明白。不知道args->This()指向的是什么对象。但在开发环境下可以看出Set函数是v8::Object::Set函数。第一个参数是key,第二个参数是value,最后一个是属性。可以理解为,之后访问“sql”这个名称就可以取出sql语句的内容。现在确实无法理解为何要如此处理,也许后面的使用代码可以告诉为什么。 javascript中创建Statement时,还提供了sql语句。New函数内的下面三句就是与此相关的。 PrepareBaton* baton = new PrepareBaton(db,stmt); baton->sql = std::string(*String::Utf8Value(sql)); db->Schedule(Work_BeginPrepare,baton); 熟悉libuv开发的人应该都了解Baton对象的作用,这是为了异步执行所需的一个对象。Database的schedule函数最主要的作用是确保Database处于一个正确的状态下。如果Database已经确定正在关闭,那么将会直接调用回调函数提示出错。如果一切条件均满足,那么Baton对象将放入uv的执行队列内等待执行。Baton内保留了javascript的回调函数。由于那是一个javascript函数,所以必须将其转换成C++可供使用的类型Local<Function>。 stmt->Wrap(args.This()); 最后就是返回对象给javascript。看到的代码是,return args.This()。由于有了之前那句,所以有理由相信现在args.This()指向的就是一个这样的对象。但Arguments是怎么做到这点的还不清楚。 sqlite3的API调用规范可以参考之前写的一篇文章:http://www.52php.cn/article/p-bgpemmva-gu.html。实际执行sql语句前,必须先调用sqlite3_prepare函数。现在看看New函数内是如何做到这步的。之前在分析javascript和C++间转换数据类型时提到了这三条语句: PrepareBaton* baton = new PrepareBaton(db,baton); 创建一个PrepareBaton对象,传入Database实例,以及javascript回调函数以及Statement对象。然后PrepareBaton对象内还保存了将要被执行的sql语句。最后一条语句是调用Database的Schedule函数,提供给Schedule函数的输入参数包括Statement的Work_BeginPrepare静态函数,以及刚创建的PrepareBaton对象。接着进入Database的Schedule函数内。Schedule函数内大部分代码都是在判断当前Database的状态是否允许立即执行传入的Work_BeginPrepare静态函数。如果Database的状态不允许执行这条sql语句,立即调用PrepareBaton对象内保存的javascript回调函数,提示有错误。或者将PrepareBaton封装成Call对象放入queue队列内,在之后的某个时间被取出执行。如果条件允许可以立即执行。再次回到Statement的Work_BeginPrepare静态函数。这个函数很简单,就是将PrepareBaton对象提供给libuv,让libuv在后台执行这个对象对应的操作。由libuv在后台调用执行的函数是Work_Prepare,Work_Prepare执行完后在主线程内被继续调用的函数是Work_AfterPrepare。接着进入Work_Prepare函数内。这个函数由libuv在后台线程内被调用执行。它的实际作用就是调用sqlite3_prepare_v2函数,Statement的status变量内存储了调用sqlite3_prepare_v2函数的结果。Work_AfterPrepare也是由libuv负责调用,只不过它是在主线程内被调用,且一定是在Work_Prepare函数被调用后。这个顺序由libuv负责协调管理。Work_AfterPrepare最主要的作用是调用Javascript的回调函数,提示是成功了还是失败了。 statement.run.apply 现在进入Statement的run方法。但C++下Statement类的哪个方法对应javascript代码下的statement.run方法呢?这又得回到Statement类的Init方法。此方法在nodejs初始化node_sqlite3模块时由nodejs调用。在Init函数内有如下语句: NODE_SET_PROTOTYPE_METHOD(constructor_template,"bind",Bind); NODE_SET_PROTOTYPE_METHOD(constructor_template,"get",Get); NODE_SET_PROTOTYPE_METHOD(constructor_template,"run",Run); NODE_SET_PROTOTYPE_METHOD(constructor_template,"all",All); NODE_SET_PROTOTYPE_METHOD(constructor_template,"each",Each); NODE_SET_PROTOTYPE_METHOD(constructor_template,"reset",Reset); NODE_SET_PROTOTYPE_METHOD(constructor_template,"finalize",Finalize); 宏的名字是:NODE_SET_PROTOTYPE_METHOD。名称已经介绍的很清楚了,这是为Statement类添加方法,就像在Javascript代码里做的那样。javascript代码下的statement.run对应的是Statement类的Run方法。 Handle<Value> Statement::Run(const Arguments& args) { HandleScope scope; Statement* stmt = ObjectWrap::Unwrap<Statement>(args.This()); Baton* baton = stmt->Bind<RunBaton>(args); if (baton == NULL) { return ThrowException(Exception::Error(String::New("Data type is not supported"))); } else { stmt->Schedule(Work_BeginRun,baton); return args.This(); } } 我从其他网上的文章中了解到,args.This()的作用就相当于javascript环境下的this。这么理解的话,此时args.This()应该指的就是Statement对象。所以此时将其转换成Statement对象完全合理。接着调用Statemtn的Bind模板函数。Bind模板函数的后两个参数都有缺省值。也就是说,此处调用Bind函数时后两个参数使用缺省值。Bind函数会返回指向Baton的指针,也就是说会创建一个RunBaton对象。RunBaton对象创建成功,就将再调用Statement的Schedule函数负责执行这个RunBaton。Statement的Schedule方法与Database的Schedule的方法相似,也是在执行RunBaton前确保所有的条件都满足,否则要么调用javascript回调函数提示错误,要么放入队列稍后执行,如果所有条件都满足立即执行RunBaton。之前也看到过调用sqlite3_prepare_v2方法时,有Work_BeginPrepare、Work_Prepare和Work_AfterPrepare这三个函数组合完成这次调用。同理,执行sqlite3_step方法时,也需要三个类似的函数:Work_BeginRun,Work_Run和Work_AfterRun。Work_BeginRun的作用就是将RunBaton提供给libuv,并通知libuv用Work_Run函数在后台处理,处理完毕后再前台调用Work_AfterRun,相关信息都存储在RunBaton内,由libuv负责调用Work_Run和Work_AfterRun时提供RunBaton对象。据此推理,其他操作应该也都有类似的三个函数。Run方法的最后是返回args.This()。现在应该知道在javascript环境下应该就是返回Statement对象。这正好匹配了Database的run方内的最后一句: statement.run.apply(statement,params).finalize(); 因为run方法可以返回statement对象,所以可以向上面那样这么写继续调用Statement的finalize方法。在继续分析finalize函数前,暂且先看看Statement的Bind模板函数。 template <class T> T* Statement::Bind(const Arguments& args,int start,int last) { if (last < 0) last = args.Length(); Local<Function> callback; if (last > start && args[last - 1]->IsFunction()) { callback = Local<Function>::Cast(args[last - 1]); last--; } T* baton = new T(this,callback); if (start < last) { if (args[start]->IsArray()) { Local<Array> array = Local<Array>::Cast(args[start]); int length = array->Length(); // Note: bind parameters start with 1. for (int i = 0,pos = 1; i < length; i++,pos++) { baton->parameters.push_back(BindParameter(array->Get(i),pos)); } } else if (!args[start]->IsObject() || args[start]->IsRegExp() || args[start]->IsDate()) { // Parameters directly in array. // Note: bind parameters start with 1. for (int i = start,pos = 1; i < last; i++,pos++) { baton->parameters.push_back(BindParameter(args[i],pos)); } } else if (args[start]->IsObject()) { Local<Object> object = Local<Object>::Cast(args[start]); Local<Array> array = object->GetPropertyNames(); int length = array->Length(); for (int i = 0; i < length; i++) { Local<Value> name = array->Get(i); if (name->IsInt32()) { baton->parameters.push_back( BindParameter(object->Get(name),name->Int32Value())); } else { baton->parameters.push_back(BindParameter(object->Get(name),*String::Utf8Value(Local<String>::Cast(name)))); } } } else { return NULL; } } return baton; } 函数内头几条代码的目的是得到javascript回调函数,以及除去最后一个回调函数参数外,最后一个参数的索引。如果只有回调函数,没有其他任何参数,那么此时last将会是0,与start相同。这种只有回调函数的情况下,创建完RunBaton后就会立即返回这个对象,而不再做任何其他处理。函数内其它都是针对除回调函数外其他参数的处理。这些处理的最终目的就是要解析出值再放入Baton的parameters成员变量中。记住,parameters中参数索引从1开始。令人不解的是,各个分支下只判断除回调函数外其他参数中的第一个参数。突然想到Database的run函数定义时有一句用来说明如何使用此函数的注释: // Database#run(sql,[callback]) 从注释中可以看出,除回调函数外以及第一个sql参数,所谓的其余参数列表其实就只有一个,只不过它可以是数组。所以,Bind函数的处理没有问题,剩下的参数只有一个。 经过分析Statement的run方法,分明看到Statement也有一个queue,也是存储Baton对象的。不知道为何Database和Statement各得有一个queue。当然Database和Statement二者的生命周期不是一致的。也许正是因为这个缘故。两个queue的具体作用还有待在分析其他代码时做进一步了解。 同理,statement.finalize对应的是Statement类的Finalize。这个函数很简单。先是置结束标志。接着清理Statement的内部queue队列。依据情况决定是否调用每个请求的回调函数。无论如何,都得确保每个被创建的Baton都必须被删除。 Database的run方法为了保持javascript下函数可以级联调用的习惯,最后返回this,这个this指的是Database对象。(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |