diff --git a/.travis.yml b/.travis.yml index 0209ef2..69bc6cb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -54,17 +54,11 @@ matrix: env: NODE_VERSION="iojs-1.8.4" PUBLISHABLE=false - os: linux env: NODE_VERSION="iojs-1.8.4" BUILD_X86=true # node abi 43 - - os: linux - env: NODE_VERSION="0.12.7" BUILD_X86=true # node abi 14 - - os: linux - env: NODE_VERSION="0.10.40" BUILD_X86=true # node abi 11 - - os: linux - env: NODE_VERSION="0.8.28" BUILD_X86=true # node abi 1 # disabled because libphp5-embed package is not on whitelist yet # https://github.com/travis-ci/apt-package-whitelist#package-approval-process # # test building against external libphp5 # - os: linux -# env: NODE_VERSION="4.1.1" EXTERNAL_LIBPHP=true PUBLISHABLE=false +# env: NODE_VERSION="4.1.2" EXTERNAL_LIBPHP=true PUBLISHABLE=false # addons: # apt: # sources: [ 'libphp5-embed:i386', 'php5-dev:i386' ] @@ -82,15 +76,6 @@ matrix: - os: osx compiler: clang env: NODE_VERSION="iojs-1.8.4" # node abi 43 - - os: osx - compiler: clang - env: NODE_VERSION="0.12.7" # node abi 14 - - os: osx - compiler: clang - env: NODE_VERSION="0.10.40" # node abi 11 - - os: osx - compiler: clang - env: NODE_VERSION="0.8.28" # node abi 1 before_install: - export PUBLISHABLE=${PUBLISHABLE:-true} diff --git a/lib/index.js b/lib/index.js index 69ddc5a..69e16c3 100644 --- a/lib/index.js +++ b/lib/index.js @@ -25,7 +25,8 @@ exports.request = function(options, cb) { source = 'require '+addslashes(options.file)+';'; } var stream = options.stream || process.stdout; - return request(source, stream).tap(function() { + var context = options.context; + return request(source, stream, context).tap(function() { // ensure the stream is flushed before promise is resolved return new Promise(function(resolve, reject) { stream.write(new Buffer(0), resolve); diff --git a/package.json b/package.json index 74e1fd2..2ff4a4a 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "host": "https://github.com/", "remote_path": "/cscott/node-php-embed/releases/download/{version}/" }, + "engines" : { "node" : ">=1.8.4" }, "dependencies": { "nan": "~2.1.0", "node-pre-gyp": "~0.6.11", @@ -39,7 +40,8 @@ ], "devDependencies": { "mocha": "~2.3.3", - "should": "~7.1.0", - "readable-stream": "~2.0.2" + "readable-stream": "~2.0.2", + "segfault-handler": "git+https://github.com/cscott/node-segfault-handler#any-signal", + "should": "~7.1.0" } } diff --git a/src/asynclockworker.h b/src/asynclockworker.h deleted file mode 100644 index 12dfe46..0000000 --- a/src/asynclockworker.h +++ /dev/null @@ -1,177 +0,0 @@ -#ifndef ASYNCLOCKWORKER_H -#define ASYNCLOCKWORKER_H -#include -#include - -namespace node_php_embed { - -class barrier_pair { - public: - explicit barrier_pair() : start(), finish() { - uv_barrier_init(&start, 2); - uv_barrier_init(&finish, 2); - } - virtual ~barrier_pair() { - uv_barrier_destroy(&start); - uv_barrier_destroy(&finish); - } - uv_barrier_t start, finish; -}; - -/* This class is similar to Nan's AsyncProgressWorker, except that - * we guarantee not to lose/discard messages sent from the worker. - */ -/* abstract */ class AsyncLockWorker : public Nan::AsyncWorker { - public: - explicit AsyncLockWorker(Nan::Callback *callback_, v8::Isolate *isolate) : AsyncWorker(callback_), asyncdata_(), waitingForLock(false), isolate_(isolate) { - async = new uv_async_t; - uv_async_init(uv_default_loop(), async, AsyncLock_); - async->data = this; - - uv_mutex_init(&async_lock); - } - - virtual ~AsyncLockWorker() { - uv_mutex_destroy(&async_lock); - // can't safely delete entries from asyncdata_, it better be empty. - } - - void WorkComplete() { - uv_mutex_lock(&async_lock); - waitingForLock = !asyncdata_.empty(); - uv_mutex_unlock(&async_lock); - - if (!waitingForLock) { - Nan::AsyncWorker::WorkComplete(); - ReallyDestroy(); - } else { - // Queue another trip through WorkQueue - uv_async_send(async); - } - } - - void WorkQueue() { - std::list newData; - bool waiting; - - uv_mutex_lock(&async_lock); - newData.splice(newData.begin(), asyncdata_); - waiting = waitingForLock; - uv_mutex_unlock(&async_lock); - - // Temporarily leave this isolate while we process the work list. - { - isolate_->Exit(); - v8::Unlocker unlocker(isolate_); - - for (std::list::iterator it = newData.begin(); - it != newData.end(); it++) { - barrier_pair *b = *it; - uv_barrier_wait(&(b->start)); - // computation in the other thread happens here. - uv_barrier_wait(&(b->finish)); - } - } - // Reclaim this isolate. - isolate_->Enter(); - - // If we were waiting for the stream to empty, perhaps it's time to - // invoke WorkComplete. - if (waiting) { - WorkComplete(); - } - } - - class ExecutionLockRequest { - friend class AsyncLockWorker; - public: - void Send(barrier_pair *b) const { - that_->SendLock_(b); - } - v8::Isolate *GetIsolate() const { - return that_->isolate_; - } - AsyncLockWorker *GetWorker() const { - return that_; - } - private: - explicit ExecutionLockRequest(AsyncLockWorker* that) : that_(that) {} - NAN_DISALLOW_ASSIGN_COPY_MOVE(ExecutionLockRequest) - AsyncLockWorker* const that_; - }; - - virtual void Execute(const ExecutionLockRequest& execLockRequest) = 0; - - virtual void ReallyDestroy() { - // The AsyncClose_ method handles deleting `this`. - uv_close(reinterpret_cast(async), AsyncClose_); - } - virtual void Destroy() { - /* do nothing -- we're going to trigger ReallyDestroy from - * WorkComplete */ - } - - private: - void Execute() /*final override*/ { - ExecutionLockRequest execLockRequest(this); - Execute(execLockRequest); - } - - void SendLock_(barrier_pair *b) { - uv_mutex_lock(&async_lock); - asyncdata_.push_back(b); - uv_mutex_unlock(&async_lock); - - uv_async_send(async); - } - - NAN_INLINE static NAUV_WORK_CB(AsyncLock_) { - AsyncLockWorker *worker = - static_cast(async->data); - worker->WorkQueue(); - } - - NAN_INLINE static void AsyncClose_(uv_handle_t* handle) { - AsyncLockWorker *worker = - static_cast(handle->data); - delete reinterpret_cast(handle); - delete worker; - } - - uv_async_t *async; - uv_mutex_t async_lock; - std::list asyncdata_; - bool waitingForLock; - v8::Isolate *isolate_; -}; - -class BarrierWait { - public: - explicit BarrierWait(const AsyncLockWorker::ExecutionLockRequest *execLockRequest) : b_() { - execLockRequest->Send(&b_); - uv_barrier_wait(&(b_.start)); - } - virtual ~BarrierWait() { - uv_barrier_wait(&(b_.finish)); - } - private: - barrier_pair b_; -}; - -/* Stack-allocated class which queues a barrier_pair, waits until it's safe to - * enter an isolate, then enters it. */ -class WaitForNode { - public: - explicit WaitForNode(const AsyncLockWorker::ExecutionLockRequest *elr) : - wait(elr), - locker(elr->GetIsolate()), - isolate_scope(elr->GetIsolate()) {} - virtual ~WaitForNode() {} - private: - BarrierWait wait; - v8::Locker locker; - v8::Isolate::Scope isolate_scope; -}; - -} -#endif diff --git a/src/asyncmessageworker.h b/src/asyncmessageworker.h new file mode 100644 index 0000000..a415460 --- /dev/null +++ b/src/asyncmessageworker.h @@ -0,0 +1,280 @@ +#ifndef ASYNCMESSAGEWORKER_H +#define ASYNCMESSAGEWORKER_H +#include +#include +#include +#include +#include "messages.h" +#include "node_php_jsobject_class.h" + +#if defined(V8_MAJOR_VERSION) && (V8_MAJOR_VERSION > 4 || \ + (V8_MAJOR_VERSION == 4 && defined(V8_MINOR_VERSION) && V8_MINOR_VERSION >= 2)) +#define USE_WEAKMAP +#define WEAKMAP_CTXT +#define WEAKMAP_MAYBE(x, d) (x) +#else +#define NativeWeakMap Map +#define WEAKMAP_CTXT v8::Isolate::GetCurrent()->GetCurrentContext(), +#define WEAKMAP_MAYBE(x, d) ((x).FromMaybe(d)) +#endif + +namespace node_php_embed { + +/* This class is similar to Nan's AsyncProgressWorker, except that + * we guarantee not to lose/discard messages sent from the worker. + */ +/* abstract */ class AsyncMessageWorker : public Nan::AsyncWorker { + public: + class MessageChannel; + explicit AsyncMessageWorker(Nan::Callback *callback) + : AsyncWorker(callback), asyncdata_(), waitingForLock_(false), + phpObjToId_(), phpObjList_(), + // id #0 is reserved for "invalid object" + nextId_(1) { + async = new uv_async_t; + uv_async_init(uv_default_loop(), async, AsyncMessage_); + async->data = this; + + uv_mutex_init(&async_lock_); + uv_mutex_init(&id_lock_); + + jsObjToId_.Reset(v8::NativeWeakMap::New(v8::Isolate::GetCurrent())); + } + + virtual ~AsyncMessageWorker() { + // invalidate any lingering js wrapper objects to this request + ClearAllJsIds(); + uv_mutex_destroy(&async_lock_); + uv_mutex_destroy(&id_lock_); + // can't safely delete entries from asyncdata_, it better be empty. + jsObjToId_.Reset(); + } + + // Map Js object to an index (JS thread only) + objid_t IdForJsObj(const v8::Local o) { + // Have we already mapped this? + Nan::HandleScope scope; + v8::Local jsObjToId = Nan::New(jsObjToId_); + if (WEAKMAP_MAYBE(jsObjToId->Has(WEAKMAP_CTXT o), false)) { + return Nan::To(WEAKMAP_MAYBE(jsObjToId->Get(WEAKMAP_CTXT o), (v8::Local) Nan::New(0))).FromJust(); + } + uv_mutex_lock(&id_lock_); + objid_t id = (nextId_++); + uv_mutex_unlock(&id_lock_); + jsObjToId->Set(WEAKMAP_CTXT o, Nan::New(id)); + SaveToPersistent(id, o); + return id; + } + v8::Local JsObjForId(objid_t id) { + Nan::EscapableHandleScope scope; + // XXX if doesn't exist, then make a wrapper (and store it in the maps) + return scope.Escape(Nan::To( + GetFromPersistent(id) + ).ToLocalChecked()); + } + // Map PHP object to an index (PHP thread only) + objid_t IdForPhpObj(zval *z) { + assert(Z_TYPE_P(z) == IS_OBJECT); + zend_object_handle handle = Z_OBJ_HANDLE_P(z); + if (phpObjToId_.count(handle)) { + return phpObjToId_.at(handle); + } + uv_mutex_lock(&id_lock_); + objid_t id = (nextId_++); + uv_mutex_unlock(&id_lock_); + if (id >= phpObjList_.size()) { phpObjList_.resize(id+1); } + // xxx clone/separate z? + Z_ADDREF_P(z); + phpObjList_[id] = z; + phpObjToId_[handle] = id; + return id; + } + // returned value is owned by objectmapper, caller should not release it. + zval * PhpObjForId(MessageChannel *channel, objid_t id TSRMLS_DC) { + if (id >= phpObjList_.size()) { phpObjList_.resize(id+1); } + ZVal z(phpObjList_[id] ZEND_FILE_LINE_CC); + if (z.IsNull()) { + node_php_jsobject_create(z.Ptr(), channel, id TSRMLS_CC); + phpObjList_[id] = z.Ptr(); + phpObjToId_[Z_OBJ_HANDLE_P(z.Ptr())] = id; + // one excess reference: owned by objectmapper + return z.Escape(); + } + // don't increment reference + return z.Ptr(); + } + // Free PHP references associated with an id (from PHP thread) + void ClearPhpId(objid_t id) { + zval *z = (id < phpObjList_.size()) ? phpObjList_[id] : NULL; + if (z) { + phpObjList_[id] = NULL; + phpObjToId_.erase(Z_OBJ_HANDLE_P(z)); + zval_ptr_dtor(&z); + } + } + void ClearAllPhpIds() { + for (objid_t id = 1; id < nextId_; id++) { + ClearPhpId(id); + } + } + // Free JS references associated with an id (from JS thread) + void ClearJsId(objid_t id) { + Nan::HandleScope scope; + Nan::MaybeLocal o = + Nan::To(GetFromPersistent(id)); + if (o.IsEmpty()) { return; } + // XXX depending on o's type, set its "is request valid" flag to false + // since there might be other references to this object. + v8::Local jsObjToId = Nan::New(jsObjToId_); + jsObjToId->Delete(WEAKMAP_CTXT o.ToLocalChecked()); + SaveToPersistent(id, Nan::Undefined()); + } + void ClearAllJsIds() { + for (objid_t id = 1; id < nextId_; id++) { + ClearJsId(id); + } + } + + void WorkComplete() { + uv_mutex_lock(&async_lock_); + waitingForLock_ = !asyncdata_.empty(); + uv_mutex_unlock(&async_lock_); + + if (!waitingForLock_) { + Nan::AsyncWorker::WorkComplete(); + ReallyDestroy(); + } else { + // Queue another trip through WorkQueue + uv_async_send(async); + } + } + + // This is in the Js Thread; dispatch requests from PHP thread. + void WorkQueue() { + std::list newData; + bool waiting; + + uv_mutex_lock(&async_lock_); + newData.splice(newData.begin(), asyncdata_); + waiting = waitingForLock_; + uv_mutex_unlock(&async_lock_); + + for (std::list::iterator it = newData.begin(); + it != newData.end(); it++) { + MessageToJs *m = *it; + // do computation in Js Thread + m->ExecuteJs(); + // PHP thread is woken at end of executeJs() + } + + // If we were waiting for the stream to empty, perhaps it's time to + // invoke WorkComplete. + if (waiting) { + WorkComplete(); + } + } + + class MessageChannel : public JsMessageChannel { + friend class AsyncMessageWorker; + public: + virtual void Send(MessageToJs *m) const { + that_->SendMessage_(m); + } + AsyncMessageWorker *GetWorker() const { + return that_; + } + virtual objid_t IdForJsObj(const v8::Local o) { + return that_->IdForJsObj(o); + } + virtual v8::Local JsObjForId(objid_t id) { + return that_->JsObjForId(id); + } + virtual objid_t IdForPhpObj(zval *o) { + return that_->IdForPhpObj(o); + } + virtual zval * PhpObjForId(objid_t id TSRMLS_DC) { + return that_->PhpObjForId(this, id TSRMLS_CC); + } + private: + explicit MessageChannel(AsyncMessageWorker* that) : that_(that) {} + NAN_DISALLOW_ASSIGN_COPY_MOVE(MessageChannel) + AsyncMessageWorker* const that_; + }; + + virtual void Execute(MessageChannel& messageChannel) = 0; + + virtual void ReallyDestroy() { + // The AsyncClose_ method handles deleting `this`. + uv_close(reinterpret_cast(async), AsyncClose_); + } + virtual void Destroy() { + /* do nothing -- we're going to trigger ReallyDestroy from + * WorkComplete */ + } + + // Limited ObjectMapper for use during subclass initialization. + protected: + class JsOnlyMapper : public ObjectMapper { + public: + JsOnlyMapper(AsyncMessageWorker *worker) : worker_(worker) { } + virtual objid_t IdForJsObj(const v8::Local o) { + return worker_->IdForJsObj(o); + } + virtual v8::Local JsObjForId(objid_t id) { + assert(false); return Nan::New(); + } + virtual objid_t IdForPhpObj(zval *o) { + assert(false); return 0; + } + virtual zval * PhpObjForId(objid_t id TSRMLS_DC) { + assert(false); return NULL; + } + private: + AsyncMessageWorker *worker_; + }; + + private: + void Execute() /*final override*/ { + MessageChannel messageChannel(this); + Execute(messageChannel); + } + + void SendMessage_(MessageToJs *b) { + uv_mutex_lock(&async_lock_); + asyncdata_.push_back(b); + uv_mutex_unlock(&async_lock_); + + uv_async_send(async); + } + + NAN_INLINE static NAUV_WORK_CB(AsyncMessage_) { + AsyncMessageWorker *worker = + static_cast(async->data); + worker->WorkQueue(); + } + + NAN_INLINE static void AsyncClose_(uv_handle_t* handle) { + AsyncMessageWorker *worker = + static_cast(handle->data); + delete reinterpret_cast(handle); + delete worker; + } + + uv_async_t *async; + uv_mutex_t async_lock_; + std::list asyncdata_; + bool waitingForLock_; + // Js Object mapping (along with GetFromPersistent/etc) + // Read/writable only from Js thread + Nan::Persistent jsObjToId_; + // PHP Object mapping + // Read/writable only from PHP thread + std::unordered_map phpObjToId_; + std::vector phpObjList_; + // Ids are allocated from both threads, so mutex is required + uv_mutex_t id_lock_; + objid_t nextId_; +}; + +} +#endif diff --git a/src/asyncstreamworker.h b/src/asyncstreamworker.h deleted file mode 100644 index 3d17137..0000000 --- a/src/asyncstreamworker.h +++ /dev/null @@ -1,137 +0,0 @@ -#ifndef ASYNCSTREAMWORKER_H -#define ASYNCSTREAMWORKER_H -#include -#include - -using namespace Nan; - -namespace node_php_embed { - -/* This class is similar to Nan's AsyncProgressWorker, except that - * we guarantee not to lose/discard messages sent from the worker. - */ -/* abstract */ class AsyncStreamWorker : public Nan::AsyncWorker { - public: - explicit AsyncStreamWorker(Callback *callback_) - : AsyncWorker(callback_), asyncdata_(), waitingForStream(false) { - async = new uv_async_t; - uv_async_init( - uv_default_loop() - , async - , AsyncStream_ - ); - async->data = this; - - uv_mutex_init(&async_lock); - } - - virtual ~AsyncStreamWorker() { - uv_mutex_destroy(&async_lock); - - for (std::list >::iterator it = asyncdata_.begin(); - it != asyncdata_.end(); it++) { - delete[] it->first; - } - asyncdata_.clear(); - } - - void WorkComplete() { - uv_mutex_lock(&async_lock); - waitingForStream = !asyncdata_.empty(); - uv_mutex_unlock(&async_lock); - - if (!waitingForStream) { - Nan::AsyncWorker::WorkComplete(); - ReallyDestroy(); - } else { - // Queue another trip through WorkStream - uv_async_send(async); - } - } - - void WorkStream() { - std::list > newData; - bool waiting; - - uv_mutex_lock(&async_lock); - newData.splice(newData.begin(), asyncdata_); - waiting = waitingForStream; - uv_mutex_unlock(&async_lock); - - for (std::list >::iterator it = newData.begin(); - it != newData.end(); it++) { - HandleStreamCallback(it->first, it->second); - delete[] it->first; - } - - // If we were waiting for the stream to empty, perhaps it's time to - // invoke WorkComplete. - if (waiting) { - WorkComplete(); - } - } - - class ExecutionStream { - friend class AsyncStreamWorker; - public: - // You could do fancy generics with templates here. - void Send(const char* data, size_t size) const { - that_->SendStream_(data, size); - } - - private: - explicit ExecutionStream(AsyncStreamWorker* that) : that_(that) {} - NAN_DISALLOW_ASSIGN_COPY_MOVE(ExecutionStream) - AsyncStreamWorker* const that_; - }; - - virtual void Execute(const ExecutionStream& stream) = 0; - virtual void HandleStreamCallback(const char *data, size_t size) = 0; - - virtual void ReallyDestroy() { - // The AsyncClose_ method handles deleting `this`. - uv_close(reinterpret_cast(async), AsyncClose_); - } - virtual void Destroy() { - /* do nothing -- we're going to trigger ReallyDestroy from - * WorkComplete */ - } - - private: - void Execute() /*final override*/ { - ExecutionStream stream(this); - Execute(stream); - } - - void SendStream_(const char *data, size_t size) { - char *new_data = new char[size]; - memcpy(new_data, data, size); - - uv_mutex_lock(&async_lock); - asyncdata_.push_back(std::pair(new_data, size)); - uv_mutex_unlock(&async_lock); - - uv_async_send(async); - } - - NAN_INLINE static NAUV_WORK_CB(AsyncStream_) { - AsyncStreamWorker *worker = - static_cast(async->data); - worker->WorkStream(); - } - - NAN_INLINE static void AsyncClose_(uv_handle_t* handle) { - AsyncStreamWorker *worker = - static_cast(handle->data); - delete reinterpret_cast(handle); - delete worker; - } - - uv_async_t *async; - uv_mutex_t async_lock; - std::list > asyncdata_; - bool waitingForStream; -}; - -} -#endif diff --git a/src/macros.h b/src/macros.h index 393f70e..e407a58 100644 --- a/src/macros.h +++ b/src/macros.h @@ -82,4 +82,24 @@ static NAN_INLINE v8::Local CAST_STRING(v8::Local v, v8:: return scope.Escape(v->IsString() ? Nan::To(v).FromMaybe(defaultValue) : defaultValue); } +/* Zend helpers */ +#if ZEND_MODULE_API_NO >= 20100409 +# define ZEND_HASH_KEY_DC , const zend_literal *key +# define ZEND_HASH_KEY_CC , key +# define ZEND_HASH_KEY_NULL , NULL +#else +# define ZEND_HASH_KEY_DC +# define ZEND_HASH_KEY_CC +# define ZEND_HASH_KEY_NULL +#endif + +/* method signatures of zend_update_property and zend_read_property were + * declared as 'char *' instead of 'const char *' before PHP 5.4 */ +#if ZEND_MODULE_API_NO >= 20100525 +# define ZEND_CONST_CHAR +#else +# define ZEND_CONST_CHAR (char *) +#endif + + #endif diff --git a/src/messages.h b/src/messages.h new file mode 100644 index 0000000..78f0375 --- /dev/null +++ b/src/messages.h @@ -0,0 +1,180 @@ +#ifndef NODE_PHP_MESSAGES_H +#define NODE_PHP_MESSAGES_H +#include +#include "values.h" + +namespace node_php_embed { + +class Message { + public: + ObjectMapper *mapper_; + Value retval_, exception_; + explicit Message(ObjectMapper *mapper) + : mapper_(mapper), retval_(), exception_() { } + virtual ~Message() { } + bool HasException() { + return !exception_.IsEmpty(); + } +}; + +class MessageToPhp : public Message { + protected: + virtual void InPhp(ObjectMapper *m TSRMLS_DC) = 0; + public: + MessageToPhp(ObjectMapper *m) : Message(m) { } + void ExecutePhp(TSRMLS_D) { + InPhp(mapper_ TSRMLS_CC); + } +}; + +class MessageToJs : public Message { + uv_barrier_t finish; + protected: + // in JS context + virtual void InJs(ObjectMapper *m) = 0; + public: + MessageToJs(ObjectMapper *m) : Message(m) { + uv_barrier_init(&finish, 2); + } + virtual ~MessageToJs() { + uv_barrier_destroy(&finish); + } + // in PHP context + void WaitForResponse() { + // XXX invoke a recursive message loop, so JS + // can call back into PHP while PHP is blocked. + uv_barrier_wait(&finish); + } + // in JS context + void ExecuteJs() { + Nan::HandleScope scope; + Nan::TryCatch tryCatch; + InJs(mapper_); + if (tryCatch.HasCaught()) { + // If an exception was thrown, set exception_ + exception_.Set(mapper_, tryCatch.Exception()); + tryCatch.Reset(); + } else if (retval_.IsEmpty() && exception_.IsEmpty()) { + // if no result, throw an exception + exception_.Set(mapper_, Nan::TypeError("no return value")); + } + // signal completion. + uv_barrier_wait(&finish); + } +}; + +class JsMessageChannel : public ObjectMapper { + public: + virtual void Send(MessageToJs *m) const = 0; +}; + +class JsGetPropertyMsg : public MessageToJs { + Value obj_; + Value name_; + public: + JsGetPropertyMsg(ObjectMapper *m, zval *obj, zval *name) + : MessageToJs(m), obj_(m, obj), name_(m, name) { } + protected: + virtual void InJs(ObjectMapper *m) { + Nan::MaybeLocal o = Nan::To(obj_.ToJs(m)); + if (o.IsEmpty()) { + return Nan::ThrowTypeError("not an object"); + } + Nan::MaybeLocal r = + Nan::Get(o.ToLocalChecked(), name_.ToJs(m)); + if (!r.IsEmpty()) { + retval_.Set(m, r.ToLocalChecked()); + } + } +}; +class JsInvokeMethodMsg : public MessageToJs { + Value obj_; + Value name_; + int argc_; + Value *argv_; + public: + JsInvokeMethodMsg(ObjectMapper *m, zval *obj, zval *name, int argc, zval **argv) + : MessageToJs(m), obj_(m, obj), name_(m, name), argc_(argc), argv_(Value::NewArray(m, argc, argv)) { } + JsInvokeMethodMsg(ObjectMapper *m, zval *obj, const char *name, int argc, zval **argv) + : MessageToJs(m), obj_(m, obj), name_(), argc_(argc), argv_(Value::NewArray(m, argc, argv)) { + name_.SetOwnedString(name, strlen(name)); + } + virtual ~JsInvokeMethodMsg() { delete[] argv_; } + // this is a bit of a hack to allow constructing a call with a Buffer + // as an argument. + Value &Argv(int n) { return argv_[n]; } + protected: + virtual void InJs(ObjectMapper *m) { + Nan::MaybeLocal o = Nan::To(obj_.ToJs(m)); + if (o.IsEmpty()) { + return Nan::ThrowTypeError("not an object"); + } + Nan::MaybeLocal method = Nan::To( + Nan::Get(o.ToLocalChecked(), name_.ToJs(m)) + .FromMaybe(Nan::Undefined()) + ); + if (method.IsEmpty()) { + return Nan::ThrowTypeError("method is not an object"); + } + v8::Local *argv = + static_cast*> + (alloca(sizeof(v8::Local) * argc_)); + for (int i=0; i; + argv[i] = argv_[i].ToJs(m); + } + Nan::MaybeLocal result = + Nan::CallAsFunction(method.ToLocalChecked(), o.ToLocalChecked(), + argc_, argv); + if (!result.IsEmpty()) { + retval_.Set(m, result.ToLocalChecked()); + } + } +}; + +class PhpGetPropertyMsg : public MessageToPhp { + Value obj_; + Value name_; + public: + PhpGetPropertyMsg(ObjectMapper *m, v8::Local obj, v8::Local name) + : MessageToPhp(m), obj_(m, obj), name_(m, name) { } + protected: + virtual void InPhp(ObjectMapper *m TSRMLS_DC) { + ZVal obj{ZEND_FILE_LINE_C}, name{ZEND_FILE_LINE_C}; + zval *r; + zend_class_entry *ce; + zend_property_info *property_info; + + obj_.ToPhp(m, obj TSRMLS_CC); name_.ToPhp(m, name TSRMLS_CC); + if (!(obj.IsObject() && name.IsString())) { + retval_.SetNull(); + return; + } + ce = Z_OBJCE_P(*obj); + property_info = zend_get_property_info(ce, *name, 1 TSRMLS_CC); + if (property_info && property_info->flags & ZEND_ACC_PUBLIC) { + r = zend_read_property(NULL, *obj, Z_STRVAL_P(*name), Z_STRLEN_P(*name), true TSRMLS_CC); + // special case uninitialized_zval_ptr and return an empty value + // (indicating that we don't intercept this property) if the + // property doesn't exist. + if (r == EG(uninitialized_zval_ptr)) { + retval_.SetEmpty(); + return; + } else { + retval_.Set(m, r); + /* We don't own the reference to php_value... unless the + * returned refcount was 0, in which case the below code + * will free it. */ + zval_add_ref(&r); + zval_ptr_dtor(&r); + return; + } + } + // XXX fallback to __get method + retval_.SetNull(); + } +}; + +} /* namespace */ + +#endif diff --git a/src/node_php_embed.cc b/src/node_php_embed.cc index 74dbcbc..aeaf630 100644 --- a/src/node_php_embed.cc +++ b/src/node_php_embed.cc @@ -1,5 +1,4 @@ #include -#include "asynclockworker.h" #include #include @@ -7,6 +6,11 @@ #include "node_php_embed.h" #include "node_php_jsobject_class.h" + +#include "asyncmessageworker.h" +#include "messages.h" +#include "values.h" + #include "macros.h" using namespace node_php_embed; @@ -14,7 +18,7 @@ static void node_php_embed_ensure_init(void); /* Per-thread storage for the module */ ZEND_BEGIN_MODULE_GLOBALS(node_php_embed) - const AsyncLockWorker::ExecutionLockRequest *execLockRequest; + AsyncMessageWorker::MessageChannel *messageChannel; ZEND_END_MODULE_GLOBALS(node_php_embed) ZEND_DECLARE_MODULE_GLOBALS(node_php_embed); @@ -28,14 +32,16 @@ ZEND_DECLARE_MODULE_GLOBALS(node_php_embed); // XXX async progress worker doesn't guarantee receipt of messages; // we need to rewrite it to use guaranteed delivery. -class node_php_embed::PhpRequestWorker : public AsyncLockWorker { +class node_php_embed::PhpRequestWorker : public AsyncMessageWorker { public: - PhpRequestWorker(Nan::Callback *callback, v8::Local stream, v8::Isolate *isolate, char *source) - : AsyncLockWorker(callback, isolate), result_(NULL) { + PhpRequestWorker(Nan::Callback *callback, v8::Local stream, v8::Local context, char *source) + : AsyncMessageWorker(callback), result_(NULL), stream_(), context_() { size_t size = strlen(source) + 1; source_ = new char[size]; memcpy(source_, source, size); - SaveToPersistent("stream", stream); + JsOnlyMapper mapper(this); + stream_.Set(&mapper, stream); + context_.Set(&mapper, context); } ~PhpRequestWorker() { delete[] source_; @@ -43,21 +49,24 @@ class node_php_embed::PhpRequestWorker : public AsyncLockWorker { delete[] result_; } } + Value &GetStream() { return stream_; } + Value &GetContext() { return context_; } // Executed inside the PHP thread. It is not safe to access V8 or // V8 data structures here, so everything we need for input and output // should go on `this`. - void Execute(const ExecutionLockRequest &elr) { - zval *retval; + void Execute(MessageChannel &messageChannel) { TSRMLS_FETCH(); if (php_request_startup(TSRMLS_C) == FAILURE) { Nan::ThrowError("can't create request"); return; } - NODE_PHP_EMBED_G(execLockRequest) = &elr; - ALLOC_INIT_ZVAL(retval); + NODE_PHP_EMBED_G(messageChannel) = &messageChannel; + { + ZVal retval{ZEND_FILE_LINE_C}; zend_first_try { - if (FAILURE == zend_eval_string_ex(source_, retval, "request", true TSRMLS_CC)) { + char eval_msg[] = { "request" }; // shows up in error messages + if (FAILURE == zend_eval_string_ex(source_, *retval, eval_msg, true TSRMLS_CC)) { if (EG(exception)) { zend_clear_exception(TSRMLS_C); SetErrorMessage(""); } } - convert_to_string(retval); - result_ = new char[Z_STRLEN_P(retval) + 1]; - memcpy(result_, Z_STRVAL_P(retval), Z_STRLEN_P(retval)); - result_[Z_STRLEN_P(retval)] = 0; + convert_to_string(*retval); + result_ = new char[Z_STRLEN_P(*retval) + 1]; + memcpy(result_, Z_STRVAL_P(*retval), Z_STRLEN_P(*retval)); + result_[Z_STRLEN_P(*retval)] = 0; } zend_catch { SetErrorMessage(""); } zend_end_try(); - zval_dtor(retval); - NODE_PHP_EMBED_G(execLockRequest) = NULL; + } + NODE_PHP_EMBED_G(messageChannel) = NULL; + // free php wrapper objects for this request + // XXX JS wrappers should be invalidated first, then probably + // one more pass through the from-JS message loop, before + // freeing the PHP size. + ClearAllPhpIds(); php_request_shutdown(NULL); } // Executed when the async work is complete. @@ -90,44 +104,55 @@ class node_php_embed::PhpRequestWorker : public AsyncLockWorker { private: char *source_; char *result_; + Value stream_; + Value context_; }; /* PHP extension metadata */ static int node_php_embed_ub_write(const char *str, unsigned int str_length TSRMLS_DC) { // Fetch the ExecutionStream object for this thread. - const AsyncLockWorker::ExecutionLockRequest *elr = NODE_PHP_EMBED_G(execLockRequest); - { - WaitForNode wait(elr); - // execute in isolate - Nan::HandleScope scope; - v8::Local stream = - elr->GetWorker()->GetFromPersistent("stream") - .As(); - v8::Local write = GET_PROPERTY(stream, "write"); - if (!write->IsFunction()) { return str_length; /* silent failure */ } - // XXX core dump here, because Node::CopyBuffer tries to access - // the node environment from the "wrong" thread. - v8::Local argv[] = { - Nan::CopyBuffer(str, str_length).ToLocalChecked() - }; - Nan::CallAsFunction(write.As(), stream, 1, argv); - } + AsyncMessageWorker::MessageChannel *messageChannel = NODE_PHP_EMBED_G(messageChannel); + PhpRequestWorker *worker = (PhpRequestWorker *) + (messageChannel->GetWorker()); + ZVal stream{ZEND_FILE_LINE_C}; + worker->GetStream().ToPhp(messageChannel, stream TSRMLS_CC); + zval buf; INIT_ZVAL(buf); // stack allocate a null zval as a placeholder + zval *args[] = { &buf }; + JsInvokeMethodMsg msg(messageChannel, stream.Ptr(), "write", 1, args); + // hack the message to pass a buffer, not a string + // (and avoid unnecessary copying by not using an "owned buffer") + msg.Argv(0).SetBuffer(str, str_length); + messageChannel->Send(&msg); + // XXX wait for response + msg.WaitForResponse(); // XXX optional? + // now return value is in msg.retval_ or msg.exception_ but + // we'll ignore that (FIXME?) return str_length; } static void node_php_embed_flush(void *server_context) { // XXX IMPLEMENT ME - // do a stream->Send('',0) and then wait for the queue to be serviced - // before returning? + // do a JsInvokeAsyncMethod of stream.write, which should add a callback + // and block until it is handled. } static void node_php_embed_register_server_variables(zval *track_vars_array TSRMLS_DC) { // Fetch the ExecutionStream object for this thread. - const AsyncLockWorker::ExecutionLockRequest *elr = NODE_PHP_EMBED_G(execLockRequest); + AsyncMessageWorker::MessageChannel *messageChannel = NODE_PHP_EMBED_G(messageChannel); + PhpRequestWorker *worker = (PhpRequestWorker *) + (messageChannel->GetWorker()); php_import_environment_variables(track_vars_array TSRMLS_CC); - php_register_variable_safe("PHP_SELF", "CSA", 3, track_vars_array TSRMLS_CC); + // Set PHP_SELF to "The filename of the currently executing script, + // relative to the document root." + // XXX + // Put PHP-wrapped version of node context object in $_SERVER['CONTEXT'] + ZVal context{ZEND_FILE_LINE_C}; + worker->GetContext().ToPhp(messageChannel, context TSRMLS_CC); + char contextName[] = { "CONTEXT" }; + php_register_variable_ex(contextName, context.Transfer(TSRMLS_C), track_vars_array TSRMLS_CC); + // XXX call a JS function passing in $_SERVER to allow init? } NAN_METHOD(setIniPath) { @@ -139,7 +164,7 @@ NAN_METHOD(setIniPath) { } NAN_METHOD(request) { - REQUIRE_ARGUMENTS(3); + REQUIRE_ARGUMENTS(4); REQUIRE_ARGUMENT_STRING(0, source); if (!*source) { return Nan::ThrowTypeError("bad string"); @@ -148,13 +173,14 @@ NAN_METHOD(request) { return Nan::ThrowTypeError("stream expected"); } v8::Local stream = info[1].As(); - if (!info[2]->IsFunction()) { + v8::Local context = info[2]; + if (!info[3]->IsFunction()) { return Nan::ThrowTypeError("callback expected"); } - Nan::Callback *callback = new Nan::Callback(info[2].As()); + Nan::Callback *callback = new Nan::Callback(info[3].As()); node_php_embed_ensure_init(); - Nan::AsyncQueueWorker(new PhpRequestWorker(callback, stream, v8::Isolate::GetCurrent(), *source)); + Nan::AsyncQueueWorker(new PhpRequestWorker(callback, stream, context, *source)); } /** PHP module housekeeping */ @@ -167,7 +193,7 @@ PHP_MINFO_FUNCTION(node_php_embed) { } static void node_php_embed_globals_ctor(zend_node_php_embed_globals *node_php_embed_globals TSRMLS_DC) { - node_php_embed_globals->execLockRequest = NULL; + node_php_embed_globals->messageChannel = NULL; } static void node_php_embed_globals_dtor(zend_node_php_embed_globals *node_php_embed_globals TSRMLS_DC) { // no clean up required @@ -198,14 +224,16 @@ zend_module_entry node_php_embed_module_entry = { /** Node module housekeeping */ static void ModuleShutdown(void *arg); +#ifdef ZTS +static void ***tsrm_ls; +#endif static bool node_php_embed_inited = false; static void node_php_embed_ensure_init(void) { if (node_php_embed_inited) { return; } node_php_embed_inited = true; - TSRMLS_FETCH(); - char *argv[] = { }; + char *argv[] = { NULL }; int argc = 0; php_embed_init(argc, argv PTSRMLS_CC); // shutdown the initially-created request diff --git a/src/node_php_jsobject_class.cc b/src/node_php_jsobject_class.cc index 321eff9..ab1e462 100644 --- a/src/node_php_jsobject_class.cc +++ b/src/node_php_jsobject_class.cc @@ -3,10 +3,19 @@ #include extern "C" { #include "php.h" +#include "Zend/zend.h" #include "Zend/zend_exceptions.h" +#include "Zend/zend_types.h" } #include "node_php_jsobject_class.h" +#include "messages.h" +#include "values.h" +#include "macros.h" + +#define USE_MAGIC_ISSET 0 + +using namespace node_php_embed; /* Class Entries */ zend_class_entry *php_ce_jsobject; @@ -14,7 +23,168 @@ zend_class_entry *php_ce_jsobject; /* Object Handlers */ static zend_object_handlers node_php_jsobject_handlers; + /* JsObject handlers */ +class JsHasPropertyMsg : public MessageToJs { + Value object_; + Value member_; + int has_set_exists_; +public: + JsHasPropertyMsg(ObjectMapper *m, objid_t objId, zval *member, int has_set_exists) + : MessageToJs(m), object_(), member_(m, member), + has_set_exists_(has_set_exists) { + object_.SetJsObject(objId); + } +protected: + virtual void InJs(ObjectMapper *m) { + retval_.SetBool(false); + v8::Local jsObj = Nan::To(object_.ToJs(m)) + .ToLocalChecked(); + v8::Local jsKey = Nan::To(member_.ToJs(m)) + .ToLocalChecked(); + v8::Local jsVal; + + /* Skip any prototype properties */ + if (Nan::HasRealNamedProperty(jsObj, jsKey).FromMaybe(false) || + Nan::HasRealNamedCallbackProperty(jsObj, jsKey).FromMaybe(false)) { + if (has_set_exists_ == 2) { + /* property_exists(), that's enough! */ + retval_.SetBool(true); + } else { + /* We need to look at the value. */ + jsVal = Nan::Get(jsObj, jsKey).FromMaybe + ((v8::Local)Nan::Undefined()); + if (has_set_exists_ == 0 ) { + /* isset(): We make 'undefined' equivalent to 'null' */ + retval_.SetBool(!(jsVal->IsNull() || jsVal->IsUndefined())); + } else { + /* empty() */ + retval_.SetBool(Nan::To(jsVal).FromMaybe(false)); + /* for PHP compatibility, [] should also be empty */ + if (jsVal->IsArray() && retval_.AsBool()) { + v8::Local array = v8::Local::Cast(jsVal); + retval_.SetBool(array->Length() != 0); + } + /* for PHP compatibility, '0' should also be empty */ + if (jsVal->IsString() && retval_.AsBool()) { + v8::Local str = Nan::To(jsVal) + .ToLocalChecked(); + if (str->Length() == 1) { + uint16_t c = 0; + str->Write(&c, 0, 1); + if (c == '0') { + retval_.SetBool(false); + } + } + } + } + } + } + } +}; + +#if USE_MAGIC_ISSET + +PHP_METHOD(JsObject, __isset) { + zval *member; + if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z/", &member) == FAILURE) { + zend_throw_exception( + zend_exception_get_default(TSRMLS_C), + "bad property name for __isset", 0 TSRMLS_CC); + return; + } + convert_to_string(member); + node_php_jsobject *obj = (node_php_jsobject *) + zend_object_store_get_object(this_ptr TSRMLS_CC); + JsHasPropertyMsg msg(obj->channel, obj->id, member, 0); + obj->channel->Send(&msg); + msg.WaitForResponse(); + // ok, result is in msg.retval_ or msg.exception_ + if (msg.HasException()) { + zend_throw_exception_ex( + zend_exception_get_default(TSRMLS_C), 0 TSRMLS_CC, + "JS exception thrown during __isset of \"%*s\"", + Z_STRLEN_P(member), Z_STRVAL_P(member)); + return; + } + RETURN_BOOL(msg.retval_.AsBool()); +} + +#else +// By overriding has_property we can implement property_exists correctly, +// and also handle empty arrays. +static int node_php_jsobject_has_property(zval *object, zval *member, int has_set_exists ZEND_HASH_KEY_DC TSRMLS_DC) { + /* param has_set_exists: + * 0 (has) whether property exists and is not NULL - isset() + * 1 (set) whether property exists and is true-ish - empty() + * 2 (exists) whether property exists - property_exists() + */ + if (Z_TYPE_P(member) != IS_STRING) { + return false; + } + node_php_jsobject *obj = (node_php_jsobject *) + zend_object_store_get_object(object TSRMLS_CC); + JsHasPropertyMsg msg(obj->channel, obj->id, member, has_set_exists); + obj->channel->Send(&msg); + msg.WaitForResponse(); + // ok, result is in msg.retval_ or msg.exception_ + if (msg.HasException()) { return false; /* sigh */ } + return msg.retval_.AsBool(); +} +#endif /* USE_MAGIC_ISSET */ + +class JsReadPropertyMsg : public MessageToJs { + Value object_; + Value member_; + int type_; +public: + JsReadPropertyMsg(ObjectMapper* m, objid_t objId, zval *member, int type) + : MessageToJs(m), object_(), member_(m, member), type_(type) { + object_.SetJsObject(objId); + } +protected: + virtual void InJs(ObjectMapper *m) { + v8::Local jsObj = Nan::To(object_.ToJs(m)) + .ToLocalChecked(); + v8::Local jsKey = Nan::To(member_.ToJs(m)) + .ToLocalChecked(); + v8::Local jsVal; + + /* Skip any prototype properties */ + if (Nan::HasRealNamedProperty(jsObj, jsKey).FromMaybe(false) || + Nan::HasRealNamedCallbackProperty(jsObj, jsKey).FromMaybe(false)) { + jsVal = Nan::Get(jsObj, jsKey).FromMaybe + ((v8::Local)Nan::Undefined()); + retval_.Set(m, jsVal); + } else { + retval_.SetNull(); + } + } +}; + + +PHP_METHOD(JsObject, __get) { + zval *member; + if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z/", &member) == FAILURE) { + zend_throw_exception(zend_exception_get_default(TSRMLS_C), "bad property name", 0 TSRMLS_CC); + return; + } + convert_to_string(member); + node_php_jsobject *obj = (node_php_jsobject *) + zend_object_store_get_object(this_ptr TSRMLS_CC); + JsReadPropertyMsg msg(obj->channel, obj->id, member, 0); + obj->channel->Send(&msg); + msg.WaitForResponse(); + // ok, result is in msg.retval_ or msg.exception_ + if (msg.HasException()) { + zend_throw_exception_ex( + zend_exception_get_default(TSRMLS_C), 0 TSRMLS_CC, + "JS exception thrown during __get of \"%*s\"", + Z_STRLEN_P(member), Z_STRVAL_P(member)); + return; + } + msg.retval_.ToPhp(obj->channel, return_value, return_value_ptr TSRMLS_CC); +} static void node_php_jsobject_free_storage(void *object, zend_object_handle handle TSRMLS_DC) { node_php_jsobject *c = (node_php_jsobject *) object; @@ -36,6 +206,12 @@ static void node_php_jsobject_free_storage(void *object, zend_object_handle hand } #endif + // XXX after we ensure that only one php wrapper for a given js id + // is created, then we could deregister the id on _free(). + // XXX actually, we need to send a message to the JS side first + // to prevent a race, since the same JS object might get used in + // another JS->PHP call first, which would revive the PHP-side wrapper. + efree(object); } @@ -46,7 +222,6 @@ static zend_object_value node_php_jsobject_new(zend_class_entry *ce TSRMLS_DC) { c = (node_php_jsobject *) ecalloc(1, sizeof(*c)); zend_object_std_init(&c->std, ce TSRMLS_CC); - new(&c->v8obj) v8::Persistent(); retval.handle = zend_objects_store_put(c, NULL, (zend_objects_free_object_storage_t) node_php_jsobject_free_storage, NULL TSRMLS_CC); retval.handlers = &node_php_jsobject_handlers; @@ -54,20 +229,17 @@ static zend_object_value node_php_jsobject_new(zend_class_entry *ce TSRMLS_DC) { return retval; } -void node_php_jsobject_create(zval *res, v8::Handle value, int flags, v8::Isolate *isolate TSRMLS_DC) { -#if 0 - node_php_ctx *ctx = (node_php_ctx *) isolate->GetData(0); -#endif +void node_php_embed::node_php_jsobject_create(zval *res, JsMessageChannel *channel, objid_t id TSRMLS_DC) { node_php_jsobject *c; object_init_ex(res, php_ce_jsobject); c = (node_php_jsobject *) zend_object_store_get_object(res TSRMLS_CC); - c->v8obj.Reset(isolate, value); + c->channel = channel; + c->id = id; #if 0 c->flags = flags; - c->ctx = ctx; c->properties = NULL; ctx->node_php_jsobjects.push_front(c); @@ -77,7 +249,7 @@ void node_php_jsobject_create(zval *res, v8::Handle value, int flags, #define STUB_METHOD(name) \ PHP_METHOD(JsObject, name) { \ zend_throw_exception( \ - zend_exception_get_default(), \ + zend_exception_get_default(TSRMLS_C), \ "Can't directly construct, serialize, or unserialize JsObject.", \ 0 TSRMLS_CC \ ); \ @@ -92,11 +264,22 @@ STUB_METHOD(__construct) STUB_METHOD(__sleep) STUB_METHOD(__wakeup) +ZEND_BEGIN_ARG_INFO_EX(node_php_jsobject_one_arg, 0, 0, 1) + ZEND_ARG_INFO(0, member) +ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_INFO_EX(node_php_jsobject_one_arg_retref, 0, 1, 1) + ZEND_ARG_INFO(0, member) +ZEND_END_ARG_INFO() + static const zend_function_entry node_php_jsobject_methods[] = { PHP_ME(JsObject, __construct, NULL, ZEND_ACC_PUBLIC|ZEND_ACC_CTOR) PHP_ME(JsObject, __sleep, NULL, ZEND_ACC_PUBLIC|ZEND_ACC_FINAL) PHP_ME(JsObject, __wakeup, NULL, ZEND_ACC_PUBLIC|ZEND_ACC_FINAL) - {NULL, NULL, NULL} +#if USE_MAGIC_ISSET + PHP_ME(JsObject, __isset, node_php_jsobject_one_arg, ZEND_ACC_PUBLIC) +#endif + PHP_ME(JsObject, __get, node_php_jsobject_one_arg_retref, ZEND_ACC_PUBLIC) + ZEND_FE_END }; @@ -113,9 +296,11 @@ PHP_MINIT_FUNCTION(node_php_jsobject_class) { node_php_jsobject_handlers.clone_obj = NULL; node_php_jsobject_handlers.cast_object = NULL; node_php_jsobject_handlers.get_property_ptr_ptr = NULL; - /* +#if !USE_MAGIC_ISSET node_php_jsobject_handlers.has_property = node_php_jsobject_has_property; - node_php_jsobject_handlers.read_property = node_php_jsobject_read_property; +#endif + //node_php_jsobject_handlers.read_property = node_php_jsobject_read_property; + /* node_php_jsobject_handlers.write_property = node_php_jsobject_write_property; node_php_jsobject_handlers.unset_property = node_php_jsobject_unset_property; node_php_jsobject_handlers.get_properties = node_php_jsobject_get_properties; diff --git a/src/node_php_jsobject_class.h b/src/node_php_jsobject_class.h index 5057ca8..19bc1c9 100644 --- a/src/node_php_jsobject_class.h +++ b/src/node_php_jsobject_class.h @@ -1,15 +1,31 @@ #ifndef NODE_PHP_OBJECT_CLASS_H #define NODE_PHP_OBJECT_CLASS_H +#include +extern "C" { +#include "php.h" +#include "Zend/zend.h" +} + +#include "values.h" /* for objid_t */ + +namespace node_php_embed { + +class JsMessageChannel; + struct node_php_jsobject { zend_object std; - v8::Persistent v8obj; + JsMessageChannel *channel; + objid_t id; }; -extern zend_class_entry *php_ce_jsobject; +/* Create a PHP proxy for a JS object. res should be allocated & inited, + * and it is owned by the caller. */ +void node_php_jsobject_create(zval *res, JsMessageChannel *channel, uint32_t id TSRMLS_DC); + +} -/* Create a PHP proxy for a JS object */ -void node_php_jsobject_create(zval *, v8::Handle, int, v8::Isolate * TSRMLS_DC); +extern zend_class_entry *php_ce_jsobject; PHP_MINIT_FUNCTION(node_php_jsobject_class); diff --git a/src/poorweakmap.h b/src/poorweakmap.h new file mode 100644 index 0000000..f0b9a1f --- /dev/null +++ b/src/poorweakmap.h @@ -0,0 +1,36 @@ +#ifndef POORWEAKMAP_H +#define POORWEAKMAP_H +#include + +/* Poor man's weak map. */ + +class PoorWeakMap { + public: + PoorWeakMap(v8::Isolate *isolate) { + int myid = id_++; + wmkey_.Reset(v8::String::Concat( + NEW_STR("php::poorweakmap::"), + Nan::To(Nan::New(myid)).ToLocalChecked())); + } + virtual ~PoorWeakMap() { wmkey_.Reset(); } + void Set(Local key, Local value) { + Nan::HandleScope scope; + return key->SetHiddenValue(Nan::New(wmkey_), value); + } + bool Has(Local key) { + Nan::HandleScope scope; + Local r = key->GetHiddenValue(Nan::New(wmkey_)); + return !r.IsEmpty(); + } + bool Delete(Local key) { + Nan::HandleScope scope; + return key->DeleteHiddenValue(Nan::New(wmkey_)); + } + Local Get(Local key) { + Nan::EscapableHandleScope scope; + return scope.Escape(key->GetHiddenValue(Nan::New(wmkey_))); + } + private: + static int id_ = 0; + Nan::Persistent wmkey_; + } diff --git a/src/values.h b/src/values.h new file mode 100644 index 0000000..2fcdb02 --- /dev/null +++ b/src/values.h @@ -0,0 +1,461 @@ +#ifndef NODE_PHP_VALUES_H +#define NODE_PHP_VALUES_H +#include +#include +#include + +#include "nan.h" + +#include "php.h" + +namespace node_php_embed { + +class NonAssignable { +private: + NonAssignable(NonAssignable const&); + NonAssignable& operator=(NonAssignable const&); +public: + NonAssignable() {} +}; + +typedef uint32_t objid_t; + +class ObjectMapper { + public: + // These two are accessed only from JS thread + // The mapper will hold persistent references to these objects + virtual objid_t IdForJsObj(const v8::Local o) = 0; + virtual v8::Local JsObjForId(objid_t id) = 0; + // These two are accessed only from PHP thread + // The mapper owns the references for these zvals. + virtual objid_t IdForPhpObj(zval *o) = 0; + virtual zval * PhpObjForId(objid_t id TSRMLS_DC) = 0; +}; + +/** Helper for PHP zvals */ +class ZVal : public NonAssignable { + public: + ZVal(ZEND_FILE_LINE_D) : transferred(false) { + ALLOC_ZVAL_REL(zvalp); + INIT_ZVAL(*zvalp); + } + ZVal(zval *z ZEND_FILE_LINE_DC) : zvalp(z), transferred(false) { + if (zvalp) { + Z_ADDREF_P(zvalp); + } else { + ALLOC_ZVAL_REL(zvalp); + INIT_ZVAL(*zvalp); + } + } + virtual ~ZVal() { + if (transferred) { + efree(zvalp); + } else { + zval_ptr_dtor(&zvalp); + } + } + inline zval * Ptr() const { return zvalp; } + inline zval ** PtrPtr() { return &zvalp; } + inline zval * Escape() { Z_ADDREF_P(zvalp); return zvalp; } + // Support a PHP calling convention where the actual zval object + // is owned by the caller, but the contents are transferred to the + // callee. + inline zval * Transfer(TSRMLS_D) { + if (IsObject()) { + zend_objects_store_add_ref(zvalp TSRMLS_CC); + } else { + transferred=true; + } + return zvalp; + } + inline zval * operator*() const { return Ptr(); } // shortcut + inline int Type() { return Z_TYPE_P(zvalp); } + inline bool IsNull() { return Type() == IS_NULL; } + inline bool IsBool() { return Type() == IS_BOOL; } + inline bool IsLong() { return Type() == IS_LONG; } + inline bool IsDouble() { return Type() == IS_DOUBLE; } + inline bool IsString() { return Type() == IS_STRING; } + inline bool IsArray() { return Type() == IS_ARRAY; } + inline bool IsObject() { return Type() == IS_OBJECT; } + inline bool IsResource() { return Type() == IS_RESOURCE; } + inline void Set(zval *z ZEND_FILE_LINE_DC) { + zval_ptr_dtor(&zvalp); + zvalp=z; + if (zvalp) { + Z_ADDREF_P(zvalp); + } else { + ALLOC_ZVAL_REL(zvalp); + INIT_ZVAL(*zvalp); + } + } + inline void SetNull() { ZVAL_NULL(zvalp); } + inline void SetBool(bool b) { ZVAL_BOOL(zvalp, b ? 1 : 0); } + inline void SetLong(long l) { ZVAL_LONG(zvalp, l); } + inline void SetDouble(double d) { ZVAL_DOUBLE(zvalp, d); } + inline void SetString(const char *str, int len, bool dup) { + ZVAL_STRINGL(zvalp, str, len, dup); + } + private: + zval *zvalp; + bool transferred; +}; + +/* A poor man's tagged union, so that we can stack allocate messages + * containing values without having to pay for heap allocation. + */ + class Value : public NonAssignable { + class Base : public NonAssignable { + public: + explicit Base() { } + virtual ~Base() { } + virtual v8::Local ToJs(ObjectMapper *m) const = 0; + virtual void ToPhp(ObjectMapper *m, zval *return_value, zval **return_value_ptr TSRMLS_DC) const = 0; + }; + class Null : public Base { + public: + explicit Null() { } + virtual v8::Local ToJs(ObjectMapper *m) const { + Nan::EscapableHandleScope scope; + return scope.Escape(Nan::Null()); + } + virtual void ToPhp(ObjectMapper *m, zval *return_value, zval **return_value_ptr TSRMLS_DC) const { + RETURN_NULL(); + } + }; + class Bool : public Base { + public: + bool value_; + explicit Bool(bool value) : value_(value) { } + virtual v8::Local ToJs(ObjectMapper *m) const { + Nan::EscapableHandleScope scope; + return scope.Escape(Nan::New(value_)); + } + virtual void ToPhp(ObjectMapper *m, zval *return_value, zval **return_value_ptr TSRMLS_DC) const { + RETURN_BOOL(value_); + } + }; + class Int : public Base { + public: + int64_t value_; + explicit Int(int64_t value) : value_(value) { } + virtual v8::Local ToJs(ObjectMapper *m) const { + Nan::EscapableHandleScope scope; + if (value_ >= 0 && value_ <= std::numeric_limits::max()) { + return scope.Escape(Nan::New((uint32_t)value_)); + } else if (value_ >= std::numeric_limits::min() && + value_ <= std::numeric_limits::max()) { + return scope.Escape(Nan::New((int32_t)value_)); + } + return scope.Escape(Nan::New((double)value_)); + } + virtual void ToPhp(ObjectMapper *m, zval *return_value, zval **return_value_ptr TSRMLS_DC) const { + if (value_ >= std::numeric_limits::min() && + value_ <= std::numeric_limits::max()) { + RETURN_LONG((long)value_); + } + RETURN_DOUBLE((double)value_); + } + }; + class Double : public Base { + double value_; + public: + explicit Double(double value) : value_(value) { } + virtual v8::Local ToJs(ObjectMapper *m) const { + Nan::EscapableHandleScope scope; + return scope.Escape(Nan::New(value_)); + } + virtual void ToPhp(ObjectMapper *m, zval *return_value, zval **return_value_ptr TSRMLS_DC) const { + RETURN_DOUBLE(value_); + } + }; + class Str : public Base { + protected: + const char *data_; + std::size_t length_; + public: + explicit Str(const char *data, std::size_t length) + : data_(data), length_(length) { } + virtual v8::Local ToJs(ObjectMapper *m) const { + Nan::EscapableHandleScope scope; + return scope.Escape(Nan::New(data_, length_).ToLocalChecked()); + } + virtual void ToPhp(ObjectMapper *m, zval *return_value, zval **return_value_ptr TSRMLS_DC) const { + RETURN_STRINGL(data_, length_, 1); + } + }; + class OStr : public Str { + // an "owned string", will copy data on creation and free it on delete. + public: + explicit OStr(const char *data, std::size_t length) + : Str(NULL, length) { + char *ndata = new char[length+1]; + memcpy(ndata, data, length); + ndata[length] = 0; + data_ = ndata; + } + virtual ~OStr() { + if (data_) { + delete[] data_; + } + } + }; + class Buf : public Str { + public: + Buf(const char *data, std::size_t length) : Str(data, length) { } + virtual v8::Local ToJs(ObjectMapper *m) const { + Nan::EscapableHandleScope scope; + return scope.Escape(Nan::CopyBuffer(data_, length_).ToLocalChecked()); + } + }; + class OBuf : public OStr { + public: + OBuf(const char *data, std::size_t length) : OStr(data, length) { } + virtual v8::Local ToJs(ObjectMapper *m) const { + Nan::EscapableHandleScope scope; + return scope.Escape(Nan::CopyBuffer(data_, length_).ToLocalChecked()); + } + }; + class Obj : public Base { + objid_t id_; + public: + explicit Obj(objid_t id) : id_(id) { } + virtual v8::Local ToJs(ObjectMapper *m) const { + Nan::EscapableHandleScope scope; + return scope.Escape(m->JsObjForId(id_)); + } + virtual void ToPhp(ObjectMapper *m, zval *return_value, zval **return_value_ptr TSRMLS_DC) const { + zval_ptr_dtor(&return_value); + *return_value_ptr = return_value = m->PhpObjForId(id_ TSRMLS_CC); + // objectmapper owns the reference returned, but we need a + // reference owned by the caller. so increment reference count. + Z_ADDREF_P(return_value); + } + }; + class JsObj : public Obj { + public: + explicit JsObj(ObjectMapper *m, v8::Local o) + : Obj(m->IdForJsObj(o)) { } + explicit JsObj(objid_t id) : Obj(id) { } + }; + class PhpObj : public Obj { + public: + explicit PhpObj(ObjectMapper *m, zval *o) + : Obj(m->IdForPhpObj(o)) { } + explicit PhpObj(objid_t id) : Obj(id) { } + }; + + public: + explicit Value() : type_(VALUE_EMPTY), empty_(0) { } + virtual ~Value() { PerhapsDestroy(); } + + explicit Value(ObjectMapper *m, v8::Local v) : type_(VALUE_EMPTY), empty_(0) { + Set(m, v); + } + explicit Value(ObjectMapper *m, zval *v) : type_(VALUE_EMPTY), empty_(0) { + Set(m, v); + } + template + static Value *NewArray(ObjectMapper *m, int argc, T* argv) { + Value *result = new Value[argc]; + for (int i=0; i v) { + if (v->IsUndefined() || v->IsNull()) { + /* fall through to the default case */ + } else if (v->IsBoolean()) { + SetBool(Nan::To(v).FromJust()); + return; + } else if (v->IsInt32() || v->IsUint32()) { + SetInt(Nan::To(v).FromJust()); + return; + } else if (v->IsNumber()) { + SetDouble(Nan::To(v).FromJust()); + return; + } else if (v->IsString()) { + Nan::Utf8String str(v); + if (*str) { + SetOwnedString(*str, str.length()); + return; + } + } else if (node::Buffer::HasInstance(v)) { + SetOwnedBuffer(node::Buffer::Data(v), node::Buffer::Length(v)); + return; + } else if (v->IsObject()) { + SetJsObject(m, Nan::To(v).ToLocalChecked()); + return; + } + // Null for all other object types. + SetNull(); + } + void Set(ObjectMapper *m, zval *v) { + long l; + switch (Z_TYPE_P(v)) { + default: + case IS_NULL: + SetNull(); + return; + case IS_BOOL: + SetBool(Z_BVAL_P(v)); + return; + case IS_LONG: + l = Z_LVAL_P(v); + if (l >= std::numeric_limits::min() && + l <= (int64_t)std::numeric_limits::max()) { + SetInt((int64_t)l); + } else { + SetDouble((double)l); + } + return; + case IS_DOUBLE: + SetDouble(Z_DVAL_P(v)); + return; + case IS_STRING: + // since PHP blocks, it is fine to let PHP + // own the buffer; avoids needless copying. + SetString(Z_STRVAL_P(v), Z_STRLEN_P(v)); + return; + case IS_OBJECT: + SetPhpObject(m, v); + return; + /* + case IS_ARRAY: + */ + } + } + void SetEmpty() { + PerhapsDestroy(); + type_ = VALUE_EMPTY; + empty_ = 0; + } + void SetNull() { + PerhapsDestroy(); + type_ = VALUE_NULL; + new (&null_) Null(); + } + void SetBool(bool value) { + PerhapsDestroy(); + type_ = VALUE_BOOL; + new (&bool_) Bool(value); + } + void SetInt(int64_t value) { + PerhapsDestroy(); + type_ = VALUE_INT; + new (&int_) Int(value); + } + void SetDouble(double value) { + PerhapsDestroy(); + type_ = VALUE_DOUBLE; + new (&double_) Double(value); + } + void SetString(const char *data, std::size_t length) { + PerhapsDestroy(); + type_ = VALUE_STR; + new (&str_) Str(data, length); + } + void SetOwnedString(const char *data, std::size_t length) { + PerhapsDestroy(); + type_ = VALUE_OSTR; + new (&ostr_) OStr(data, length); + } + void SetBuffer(const char *data, std::size_t length) { + PerhapsDestroy(); + type_ = VALUE_BUF; + new (&buf_) Buf(data, length); + } + void SetOwnedBuffer(const char *data, std::size_t length) { + PerhapsDestroy(); + type_ = VALUE_OBUF; + new (&obuf_) OBuf(data, length); + } + void SetJsObject(ObjectMapper *m, v8::Local o) { + SetJsObject(m->IdForJsObj(o)); + } + void SetJsObject(objid_t id) { + PerhapsDestroy(); + type_ = VALUE_JSOBJ; + new (&jsobj_) JsObj(id); + } + void SetPhpObject(ObjectMapper *m, zval *o) { + SetPhpObject(m->IdForPhpObj(o)); + } + void SetPhpObject(objid_t id) { + PerhapsDestroy(); + type_ = VALUE_PHPOBJ; + new (&phpobj_) PhpObj(id); + } + + v8::Local ToJs(ObjectMapper *m) { + // should we create a new escapablehandlescope here? + return AsBase().ToJs(m); + } + // caller owns the zval + void ToPhp(ObjectMapper *m, zval *return_value, zval **return_value_ptr TSRMLS_DC) { + AsBase().ToPhp(m, return_value, return_value_ptr TSRMLS_CC); + } + // caller owns the ZVal, and is responsible for freeing it. + inline void ToPhp(ObjectMapper *m, ZVal &z TSRMLS_DC) { + ToPhp(m, z.Ptr(), z.PtrPtr() TSRMLS_CC); + } + bool IsEmpty() { + return (type_ == VALUE_EMPTY); + } + bool AsBool() { + switch(type_) { + case VALUE_BOOL: + return bool_.value_; + case VALUE_INT: + return int_.value_ != 0; + default: + return false; + } + } + private: + void PerhapsDestroy() { + if (!IsEmpty()) { + AsBase().~Base(); + } + type_ = VALUE_EMPTY; + } + enum ValueTypes { + VALUE_EMPTY, VALUE_NULL, VALUE_BOOL, VALUE_INT, VALUE_DOUBLE, + VALUE_STR, VALUE_OSTR, VALUE_BUF, VALUE_OBUF, + VALUE_JSOBJ, VALUE_PHPOBJ + } type_; + union { + int empty_; Null null_; Bool bool_; Int int_; Double double_; + Str str_; OStr ostr_; Buf buf_; OBuf obuf_; + JsObj jsobj_; PhpObj phpobj_; + }; + const Base &AsBase() { + switch(type_) { + default: + // should never get here. + case VALUE_NULL: + return null_; + case VALUE_BOOL: + return bool_; + case VALUE_INT: + return int_; + case VALUE_DOUBLE: + return double_; + case VALUE_STR: + return str_; + case VALUE_OSTR: + return ostr_; + case VALUE_BUF: + return buf_; + case VALUE_OBUF: + return obuf_; + case VALUE_JSOBJ: + return jsobj_; + case VALUE_PHPOBJ: + return phpobj_; + } + } +}; + +} +#endif diff --git a/test-setup.js b/test-setup.js new file mode 100644 index 0000000..67a91f8 --- /dev/null +++ b/test-setup.js @@ -0,0 +1,5 @@ +var SegfaultHandler = require('segfault-handler'); +// Listen for SIGSEGV. +SegfaultHandler.registerHandler("segfault.log"); +// Listen for SIGABRT, too. +SegfaultHandler.registerHandler(SegfaultHandler.SIGABRT); diff --git a/test/context.js b/test/context.js new file mode 100644 index 0000000..c886ef9 --- /dev/null +++ b/test/context.js @@ -0,0 +1,122 @@ +var path = require('path'); +var stream = require('readable-stream'); // for node 0.8.x compatability +var util = require('util'); + +require('should'); + +var StringStream = function(opts) { + opts = opts || {}; + StringStream.super_.call(this, opts); + this._result = ''; + this._encoding = opts.encoding || 'utf8'; +}; +util.inherits(StringStream, stream.Writable); +StringStream.prototype._write = function(chunk, encoding, callback) { + this._result += chunk.toString(this._encoding); + return callback(null); +}; +StringStream.prototype.toString = function() { + return this._result; +}; + +describe('Passing context object from JS to PHP', function() { + var php = require('../'); + it('should pass all basic data types from JS to PHP', function() { + var out = new StringStream(); + return php.request({ + file: path.join(__dirname, 'context.php'), + stream: out, + context: { + a: false, + b: true, + c: -42, + d: (((1<<30)-1)*4), + e: 1.5, + f: 'abcdef \uD83D\uDCA9', + g: { f: 1 }, + h: function fname(x) { return x; }, + i: new Buffer('abc', 'utf8') + } + }).then(function(v) { + out.toString().replace(/int\(4294967292\)/,'float(4294967292)') + .should.equal( + 'bool(false)\n' + + 'bool(true)\n' + + 'int(-42)\n' + + 'float(4294967292)\n' + + 'float(1.5)\n' + + 'string(11) "abcdef \uD83D\uDCA9"\n' + + 'int(1)\n' + + 'string(5) "fname"\n' + + 'string(3) "abc"\n' + ); + }); + }); + it('should implement isset(), empty(), and property_exists', function() { + var out = new StringStream(); + return php.request({ + file: path.join(__dirname, 'context2.php'), + stream: out, + context: { + a: 0, + b: 42, + c: null, + d: undefined, + e: '0', + f: '1' + } + }).then(function(v) { + out.toString().should.equal( + 'a: int(0)\n' + + 'isset: bool(true)\n' + + 'empty: bool(true)\n' + + 'exists: bool(true)\n' + + '\n' + + 'b: int(42)\n' + + 'isset: bool(true)\n' + + 'empty: bool(false)\n' + + 'exists: bool(true)\n' + + '\n' + + 'c: NULL\n' + + 'isset: bool(false)\n' + + 'empty: bool(true)\n' + + 'exists: bool(true)\n' + + '\n' + + 'd: NULL\n' + + 'isset: bool(false)\n' + + 'empty: bool(true)\n' + + 'exists: bool(true)\n' + + '\n' + + 'e: string(1) "0"\n' + + 'isset: bool(true)\n' + + 'empty: bool(true)\n' + + 'exists: bool(true)\n' + + '\n' + + 'f: string(1) "1"\n' + + 'isset: bool(true)\n' + + 'empty: bool(false)\n' + + 'exists: bool(true)\n' + + '\n' + + 'g: NULL\n' + + 'isset: bool(false)\n' + + 'empty: bool(true)\n' + + 'exists: bool(false)\n' + + '\n' + ); + }); + }); + it('should handle exceptions in getters', function() { + var out = new StringStream(); + var context = {}; + Object.defineProperty(context, 'a', { get: function() { + throw new Error('boo'); + } }); + return php.request({ + source: "call_user_func(function () { try { var_dump($_SERVER['CONTEXT']->a); } catch (Exception $e) { echo 'exception caught'; } })", + stream: out, + context: context + }).then(function() { + out.toString().should.equal('exception caught'); + }); + }); +}); diff --git a/test/context.php b/test/context.php new file mode 100644 index 0000000..b1fc4d1 --- /dev/null +++ b/test/context.php @@ -0,0 +1,12 @@ +a); +var_dump($c->b); +var_dump($c->c); +var_dump($c->d); +var_dump($c->e); +var_dump($c->f); +var_dump($c->g->f); +var_dump($c->h->name); +var_dump($c->i); +?> diff --git a/test/context2.php b/test/context2.php new file mode 100644 index 0000000..525a4bd --- /dev/null +++ b/test/context2.php @@ -0,0 +1,46 @@ +a); +echo "isset: "; var_dump(isset($c->a)); +echo "empty: "; var_dump(empty($c->a)); +echo "exists: "; var_dump(property_exists($c, "a")); +echo "\n"; + +echo "b: "; var_dump($c->b); +echo "isset: "; var_dump(isset($c->b)); +echo "empty: "; var_dump(empty($c->b)); +echo "exists: "; var_dump(property_exists($c, "b")); +echo "\n"; + +echo "c: "; var_dump($c->c); +echo "isset: "; var_dump(isset($c->c)); +echo "empty: "; var_dump(empty($c->c)); +echo "exists: "; var_dump(property_exists($c, "c")); +echo "\n"; + +echo "d: "; var_dump($c->d); +echo "isset: "; var_dump(isset($c->d)); +echo "empty: "; var_dump(empty($c->d)); +echo "exists: "; var_dump(property_exists($c, "d")); +echo "\n"; + +echo "e: "; var_dump($c->e); +echo "isset: "; var_dump(isset($c->e)); +echo "empty: "; var_dump(empty($c->e)); +echo "exists: "; var_dump(property_exists($c, "e")); +echo "\n"; + +echo "f: "; var_dump($c->f); +echo "isset: "; var_dump(isset($c->f)); +echo "empty: "; var_dump(empty($c->f)); +echo "exists: "; var_dump(property_exists($c, "f")); +echo "\n"; + +echo "g: "; var_dump($c->g); +echo "isset: "; var_dump(isset($c->g)); +echo "empty: "; var_dump(empty($c->g)); +echo "exists: "; var_dump(property_exists($c, "g")); +echo "\n"; + +?> diff --git a/test/mocha.opts b/test/mocha.opts index af53e24..e80fc12 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1,2 +1,3 @@ --check-leaks --reporter spec +--require test-setup.js