From 3170fd2baed1d773713a5222aa06b90c0e8f5fb8 Mon Sep 17 00:00:00 2001 From: "C. Scott Ananian" Date: Fri, 16 Oct 2015 03:01:16 -0400 Subject: [PATCH 01/10] Redo streaming from PHP as message-passing. --- src/asynclockworker.h | 177 ---------------- src/asyncmessageworker.h | 163 +++++++++++++++ src/asyncstreamworker.h | 137 ------------ src/macros.h | 20 ++ src/messages.h | 178 ++++++++++++++++ src/node_php_embed.cc | 83 ++++---- src/node_php_jsobject_class.cc | 147 ++++++++++++- src/node_php_jsobject_class.h | 22 +- src/values.h | 371 +++++++++++++++++++++++++++++++++ 9 files changed, 932 insertions(+), 366 deletions(-) delete mode 100644 src/asynclockworker.h create mode 100644 src/asyncmessageworker.h delete mode 100644 src/asyncstreamworker.h create mode 100644 src/messages.h create mode 100644 src/values.h 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..9bd4c1d --- /dev/null +++ b/src/asyncmessageworker.h @@ -0,0 +1,163 @@ +#ifndef ASYNCMESSAGEWORKER_H +#define ASYNCMESSAGEWORKER_H +#include +#include +#include "messages.h" + +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: + explicit AsyncMessageWorker(Nan::Callback *callback_) + : AsyncWorker(callback_), asyncdata_(), waitingForLock_(false), + nextId_(0) { + async = new uv_async_t; + uv_async_init(uv_default_loop(), async, AsyncMessage_); + async->data = this; + + uv_mutex_init(&async_lock); + + objToId_.Reset(v8::NativeWeakMap::New(v8::Isolate::GetCurrent())); + } + + virtual ~AsyncMessageWorker() { + uv_mutex_destroy(&async_lock); + // can't safely delete entries from asyncdata_, it better be empty. + objToId_.Reset(); + } + + // Map Js object to an index + uint32_t IdForJsObj(const v8::Local o) { + // Have we already mapped this? + Nan::HandleScope scope; + v8::Local objToId = Nan::New(objToId_); + if (objToId->Has(o)) { + return Nan::To(objToId->Get(o)).FromJust(); + } + uint32_t r = (nextId_++); + objToId->Set(o, Nan::New(r)); + SaveToPersistent(r, o); + return r; + } + + 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 uint32_t IdForJsObj(const v8::Local o) { + return that_->IdForJsObj(o); + } + virtual v8::Local GetJs(uint32_t id) { + Nan::EscapableHandleScope scope; + return scope.Escape(Nan::To( + that_->GetFromPersistent(id) + ).ToLocalChecked()); + } + virtual uint32_t IdForPhpObj(const zval *o) { + return 0; // XXX + } + virtual void GetPhp(uint32_t id, zval *return_value TSRMLS_DC) { + // XXX use GetFromPersistent, and then unwrap v8::External? + } + 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 */ + } + + 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) + Nan::Persistent objToId_; + uint32_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..417e4ba --- /dev/null +++ b/src/messages.h @@ -0,0 +1,178 @@ +#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, uint32_t objId, const char *name, int argc, zval **argv) + : MessageToJs(m), obj_(), name_(), argc_(argc), argv_(Value::NewArray(m, argc, argv)) { + obj_.SetJsObject(objId); + name_.SetOwnedString(name, strlen(name)); + } + virtual ~JsInvokeMethodMsg() { delete[] argv_; } + 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, name; + 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..5da0faa 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,14 @@ 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, char *source) + : AsyncMessageWorker(callback), result_(NULL) { size_t size = strlen(source) + 1; source_ = new char[size]; memcpy(source_, source, size); - SaveToPersistent("stream", stream); + streamId_ = IdForJsObj(stream); } ~PhpRequestWorker() { delete[] source_; @@ -43,21 +47,23 @@ class node_php_embed::PhpRequestWorker : public AsyncLockWorker { delete[] result_; } } + uint32_t GetStreamId() { return streamId_; } // 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_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; php_request_shutdown(NULL); } // Executed when the async work is complete. @@ -90,43 +96,42 @@ class node_php_embed::PhpRequestWorker : public AsyncLockWorker { private: char *source_; char *result_; + uint32_t streamId_; }; /* 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); + uint32_t streamId = ((PhpRequestWorker*) messageChannel->GetWorker()) + ->GetStreamId(); + zval buf; + INIT_ZVAL(buf); ZVAL_STRINGL(&buf, str, str_length, 0); + zval *args[] = { &buf }; + // XXX, would be better to pass this as a buffer, not a string. + JsInvokeMethodMsg msg(messageChannel, streamId, "write", 1, args); + 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); php_import_environment_variables(track_vars_array TSRMLS_CC); + // XXX put PHP-wrapped version of node context object in the + // $SERVER variable. php_register_variable_safe("PHP_SELF", "CSA", 3, track_vars_array TSRMLS_CC); } @@ -154,7 +159,7 @@ NAN_METHOD(request) { Nan::Callback *callback = new Nan::Callback(info[2].As()); node_php_embed_ensure_init(); - Nan::AsyncQueueWorker(new PhpRequestWorker(callback, stream, v8::Isolate::GetCurrent(), *source)); + Nan::AsyncQueueWorker(new PhpRequestWorker(callback, stream, *source)); } /** PHP module housekeeping */ @@ -167,7 +172,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 diff --git a/src/node_php_jsobject_class.cc b/src/node_php_jsobject_class.cc index 321eff9..8603f18 100644 --- a/src/node_php_jsobject_class.cc +++ b/src/node_php_jsobject_class.cc @@ -3,10 +3,17 @@ #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" + +using namespace node_php_embed; /* Class Entries */ zend_class_entry *php_ce_jsobject; @@ -14,7 +21,130 @@ 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, uint32_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); + } + } + } + } + } + } + } +}; + +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(); +} + +class JsReadPropertyMsg : public MessageToJs { + Value object_; + Value member_; + int type_; +public: + JsReadPropertyMsg(ObjectMapper* m, uint32_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(); + } + } +}; + +static zval *node_php_jsobject_read_property(zval *object, zval *member, int type ZEND_HASH_KEY_DC TSRMLS_DC) { + ZVal retval; + if (Z_TYPE_P(member) != IS_STRING) { + return retval.Escape(); + } + node_php_jsobject *obj = (node_php_jsobject *) + zend_object_store_get_object(object TSRMLS_CC); + JsReadPropertyMsg msg(obj->channel, obj->id, member, type); + obj->channel->Send(&msg); + msg.WaitForResponse(); + // ok, result is in msg.retval_ or msg.exception_ + if (msg.HasException()) { msg.retval_.SetNull(); /* sigh */ } + msg.retval_.ToPhp(obj->channel, *retval TSRMLS_CC); + return retval.Escape(); +} + 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 +166,9 @@ 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(). + efree(object); } @@ -46,7 +179,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 +186,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, ObjectMapper *mapper, uint32_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 = static_cast(mapper); + c->id = id; #if 0 c->flags = flags; - c->ctx = ctx; c->properties = NULL; ctx->node_php_jsobjects.push_front(c); @@ -77,7 +206,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 \ ); \ @@ -113,9 +242,9 @@ 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; - /* node_php_jsobject_handlers.has_property = node_php_jsobject_has_property; 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..408af4b 100644 --- a/src/node_php_jsobject_class.h +++ b/src/node_php_jsobject_class.h @@ -1,15 +1,29 @@ #ifndef NODE_PHP_OBJECT_CLASS_H #define NODE_PHP_OBJECT_CLASS_H +#include +extern "C" { +#include "php.h" +#include "Zend/zend.h" +} + +namespace node_php_embed { + +class JsMessageChannel; +class ObjectMapper; + struct node_php_jsobject { zend_object std; - v8::Persistent v8obj; + JsMessageChannel *channel; + uint32_t id; }; -extern zend_class_entry *php_ce_jsobject; - /* Create a PHP proxy for a JS object */ -void node_php_jsobject_create(zval *, v8::Handle, int, v8::Isolate * TSRMLS_DC); +void node_php_jsobject_create(zval *res, ObjectMapper *mapper, uint32_t id TSRMLS_DC); + +} + +extern zend_class_entry *php_ce_jsobject; PHP_MINIT_FUNCTION(node_php_jsobject_class); diff --git a/src/values.h b/src/values.h new file mode 100644 index 0000000..b4a0cb3 --- /dev/null +++ b/src/values.h @@ -0,0 +1,371 @@ +#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() {} +}; + +class ObjectMapper { + public: + virtual uint32_t IdForJsObj(const v8::Local o) = 0; + virtual uint32_t IdForPhpObj(const zval *o) = 0; + virtual void GetPhp(uint32_t id, zval *return_value TSRMLS_DC) = 0; + virtual v8::Local GetJs(uint32_t id) = 0; +}; + +/** Helper for PHP zvals */ +class ZVal : public NonAssignable { + public: + ZVal() { ALLOC_INIT_ZVAL(zvalp); } + virtual ~ZVal() { zval_ptr_dtor(&zvalp); } + inline zval * operator*() const { return zvalp; } + inline zval * Escape() { Z_ADDREF_P(zvalp); return zvalp; } + 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 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; +}; + +/* 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 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 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 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 TSRMLS_DC) const { + RETURN_LONG((long)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 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 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(estrndup(data, length+1), length) { } + virtual ~OStr() { + if (data_) { + efree(const_cast(data_)); + } + } + virtual void ToPhp(ObjectMapper *m, zval *return_value TSRMLS_DC) const { + RETVAL_STRINGL(data_, length_, 0/*no dup required*/); + //data_ = NULL; + } + }; + class JsObj : public Base { + uint32_t id_; + public: + explicit JsObj(ObjectMapper *m, v8::Local o) + : id_(m->IdForJsObj(o)) { } + explicit JsObj(uint32_t id) + : id_(id) { } + virtual v8::Local ToJs(ObjectMapper *m) const { + Nan::EscapableHandleScope scope; + return scope.Escape(m->GetJs(id_)); + } + virtual void ToPhp(ObjectMapper *m, zval *return_value TSRMLS_DC) const { + // wrap js object + node_php_jsobject_create(return_value, m, id_ TSRMLS_CC); + } + }; + class PhpObj : public Base { + uint32_t id_; + public: + explicit PhpObj(ObjectMapper *m, zval *o) + : id_(m->IdForPhpObj(o)) { } + explicit PhpObj(uint32_t id) + : id_(id) { } + virtual v8::Local ToJs(ObjectMapper *m) const { + Nan::EscapableHandleScope scope; + // XXX wrap php object + return scope.Escape(Nan::Null()); + } + virtual void ToPhp(ObjectMapper *m, zval *return_value TSRMLS_DC) const { + m->GetPhp(id_, return_value TSRMLS_CC); + } + }; + + 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 (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 SetJsObject(ObjectMapper *m, v8::Local o) { + SetJsObject(m->IdForJsObj(o)); + } + void SetJsObject(uint32_t id) { + PerhapsDestroy(); + type_ = VALUE_JSOBJ; + new (&jsobj_) JsObj(id); + } + void SetPhpObject(ObjectMapper *m, zval *o) { + SetPhpObject(m->IdForPhpObj(o)); + } + void SetPhpObject(uint32_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); + } + void ToPhp(ObjectMapper *m, zval *return_value TSRMLS_DC) { + AsBase().ToPhp(m, return_value 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_JSOBJ, VALUE_PHPOBJ + } type_; + union { + int empty_; Null null_; Bool bool_; Int int_; Double double_; + Str str_; OStr ostr_; 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_JSOBJ: + return jsobj_; + case VALUE_PHPOBJ: + return phpobj_; + } + } +}; + +} +#endif From bda9516e57d8583156cc3825e5c3ad69a01fa217 Mon Sep 17 00:00:00 2001 From: "C. Scott Ananian" Date: Sun, 18 Oct 2015 23:01:57 -0400 Subject: [PATCH 02/10] Add context object to pass data from JavaScript to PHP. --- lib/index.js | 3 +- src/asyncmessageworker.h | 173 ++++++++++++++++++++++++++------- src/messages.h | 9 +- src/node_php_embed.cc | 53 +++++++--- src/node_php_jsobject_class.cc | 15 +-- src/node_php_jsobject_class.h | 10 +- src/values.h | 135 ++++++++++++++++--------- test/context.js | 51 ++++++++++ test/context.php | 10 ++ 9 files changed, 346 insertions(+), 113 deletions(-) create mode 100644 test/context.js create mode 100644 test/context.php 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/src/asyncmessageworker.h b/src/asyncmessageworker.h index 9bd4c1d..d6e9ca5 100644 --- a/src/asyncmessageworker.h +++ b/src/asyncmessageworker.h @@ -1,8 +1,11 @@ #ifndef ASYNCMESSAGEWORKER_H #define ASYNCMESSAGEWORKER_H #include +#include #include +#include #include "messages.h" +#include "node_php_jsobject_class.h" namespace node_php_embed { @@ -11,42 +14,117 @@ namespace node_php_embed { */ /* abstract */ class AsyncMessageWorker : public Nan::AsyncWorker { public: - explicit AsyncMessageWorker(Nan::Callback *callback_) - : AsyncWorker(callback_), asyncdata_(), waitingForLock_(false), - nextId_(0) { + 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(&async_lock_); + uv_mutex_init(&id_lock_); - objToId_.Reset(v8::NativeWeakMap::New(v8::Isolate::GetCurrent())); + jsObjToId_.Reset(v8::NativeWeakMap::New(v8::Isolate::GetCurrent())); } virtual ~AsyncMessageWorker() { - uv_mutex_destroy(&async_lock); + // 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. - objToId_.Reset(); + jsObjToId_.Reset(); } - // Map Js object to an index - uint32_t IdForJsObj(const v8::Local o) { + // 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 objToId = Nan::New(objToId_); - if (objToId->Has(o)) { - return Nan::To(objToId->Get(o)).FromJust(); + v8::Local jsObjToId = Nan::New(jsObjToId_); + if (jsObjToId->Has(o)) { + return Nan::To(jsObjToId->Get(o)).FromJust(); + } + uv_mutex_lock(&id_lock_); + objid_t id = (nextId_++); + uv_mutex_unlock(&id_lock_); + jsObjToId->Set(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) { + if (phpObjToId_.count(z)) { + return phpObjToId_.at(z); + } + uv_mutex_lock(&id_lock_); + objid_t id = (nextId_++); + uv_mutex_unlock(&id_lock_); + Z_ADDREF_P(z); + if (id >= phpObjList_.size()) { phpObjList_.resize(id+1); } + phpObjList_[id] = z; + phpObjToId_[z] = 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.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); + 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(o.ToLocalChecked()); + SaveToPersistent(id, Nan::Undefined()); + } + void ClearAllJsIds() { + for (objid_t id = 1; id < nextId_; id++) { + ClearJsId(id); } - uint32_t r = (nextId_++); - objToId->Set(o, Nan::New(r)); - SaveToPersistent(r, o); - return r; } void WorkComplete() { - uv_mutex_lock(&async_lock); + uv_mutex_lock(&async_lock_); waitingForLock_ = !asyncdata_.empty(); - uv_mutex_unlock(&async_lock); + uv_mutex_unlock(&async_lock_); if (!waitingForLock_) { Nan::AsyncWorker::WorkComplete(); @@ -62,10 +140,10 @@ namespace node_php_embed { std::list newData; bool waiting; - uv_mutex_lock(&async_lock); + uv_mutex_lock(&async_lock_); newData.splice(newData.begin(), asyncdata_); waiting = waitingForLock_; - uv_mutex_unlock(&async_lock); + uv_mutex_unlock(&async_lock_); for (std::list::iterator it = newData.begin(); it != newData.end(); it++) { @@ -91,20 +169,17 @@ namespace node_php_embed { AsyncMessageWorker *GetWorker() const { return that_; } - virtual uint32_t IdForJsObj(const v8::Local o) { + virtual objid_t IdForJsObj(const v8::Local o) { return that_->IdForJsObj(o); } - virtual v8::Local GetJs(uint32_t id) { - Nan::EscapableHandleScope scope; - return scope.Escape(Nan::To( - that_->GetFromPersistent(id) - ).ToLocalChecked()); + virtual v8::Local JsObjForId(objid_t id) { + return that_->JsObjForId(id); } - virtual uint32_t IdForPhpObj(const zval *o) { - return 0; // XXX + virtual objid_t IdForPhpObj(zval *o) { + return that_->IdForPhpObj(o); } - virtual void GetPhp(uint32_t id, zval *return_value TSRMLS_DC) { - // XXX use GetFromPersistent, and then unwrap v8::External? + virtual zval * PhpObjForId(objid_t id TSRMLS_DC) { + return that_->PhpObjForId(this, id TSRMLS_CC); } private: explicit MessageChannel(AsyncMessageWorker* that) : that_(that) {} @@ -123,6 +198,27 @@ namespace node_php_embed { * 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); @@ -130,9 +226,9 @@ namespace node_php_embed { } void SendMessage_(MessageToJs *b) { - uv_mutex_lock(&async_lock); + uv_mutex_lock(&async_lock_); asyncdata_.push_back(b); - uv_mutex_unlock(&async_lock); + uv_mutex_unlock(&async_lock_); uv_async_send(async); } @@ -151,12 +247,19 @@ namespace node_php_embed { } uv_async_t *async; - uv_mutex_t async_lock; + uv_mutex_t async_lock_; std::list asyncdata_; bool waitingForLock_; // Js Object mapping (along with GetFromPersistent/etc) - Nan::Persistent objToId_; - uint32_t nextId_; + // 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_; }; } diff --git a/src/messages.h b/src/messages.h index 417e4ba..33444aa 100644 --- a/src/messages.h +++ b/src/messages.h @@ -95,9 +95,8 @@ class JsInvokeMethodMsg : public MessageToJs { 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, uint32_t objId, const char *name, int argc, zval **argv) - : MessageToJs(m), obj_(), name_(), argc_(argc), argv_(Value::NewArray(m, argc, argv)) { - obj_.SetJsObject(objId); + 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_; } @@ -138,12 +137,12 @@ class PhpGetPropertyMsg : public MessageToPhp { : MessageToPhp(m), obj_(m, obj), name_(m, name) { } protected: virtual void InPhp(ObjectMapper *m TSRMLS_DC) { - ZVal obj, name; + 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); + obj_.ToPhp(m, obj TSRMLS_CC); name_.ToPhp(m, name TSRMLS_CC); if (!(obj.IsObject() && name.IsString())) { retval_.SetNull(); return; diff --git a/src/node_php_embed.cc b/src/node_php_embed.cc index 5da0faa..0941cd2 100644 --- a/src/node_php_embed.cc +++ b/src/node_php_embed.cc @@ -34,12 +34,14 @@ ZEND_DECLARE_MODULE_GLOBALS(node_php_embed); // we need to rewrite it to use guaranteed delivery. class node_php_embed::PhpRequestWorker : public AsyncMessageWorker { public: - PhpRequestWorker(Nan::Callback *callback, v8::Local stream, char *source) - : AsyncMessageWorker(callback), 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); - streamId_ = IdForJsObj(stream); + JsOnlyMapper mapper(this); + stream_.Set(&mapper, stream); + context_.Set(&mapper, context); } ~PhpRequestWorker() { delete[] source_; @@ -47,7 +49,8 @@ class node_php_embed::PhpRequestWorker : public AsyncMessageWorker { delete[] result_; } } - uint32_t GetStreamId() { return streamId_; } + 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 @@ -60,7 +63,7 @@ class node_php_embed::PhpRequestWorker : public AsyncMessageWorker { } NODE_PHP_EMBED_G(messageChannel) = &messageChannel; { - ZVal retval; + ZVal retval(ZEND_FILE_LINE_C); zend_first_try { char eval_msg[] = { "request" }; // shows up in error messages if (FAILURE == zend_eval_string_ex(source_, *retval, eval_msg, true TSRMLS_CC)) { @@ -80,6 +83,11 @@ class node_php_embed::PhpRequestWorker : public AsyncMessageWorker { } zend_end_try(); } 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. @@ -96,7 +104,8 @@ class node_php_embed::PhpRequestWorker : public AsyncMessageWorker { private: char *source_; char *result_; - uint32_t streamId_; + Value stream_; + Value context_; }; /* PHP extension metadata */ @@ -104,13 +113,16 @@ class node_php_embed::PhpRequestWorker : public AsyncMessageWorker { static int node_php_embed_ub_write(const char *str, unsigned int str_length TSRMLS_DC) { // Fetch the ExecutionStream object for this thread. AsyncMessageWorker::MessageChannel *messageChannel = NODE_PHP_EMBED_G(messageChannel); - uint32_t streamId = ((PhpRequestWorker*) messageChannel->GetWorker()) - ->GetStreamId(); + PhpRequestWorker *worker = (PhpRequestWorker *) + (messageChannel->GetWorker()); + ZVal stream(ZEND_FILE_LINE_C); + worker->GetStream().ToPhp(messageChannel, stream TSRMLS_CC); + // Creating buf "the hard way" to avoid unnecessary copying of str. zval buf; INIT_ZVAL(buf); ZVAL_STRINGL(&buf, str, str_length, 0); zval *args[] = { &buf }; // XXX, would be better to pass this as a buffer, not a string. - JsInvokeMethodMsg msg(messageChannel, streamId, "write", 1, args); + JsInvokeMethodMsg msg(messageChannel, stream.Ptr(), "write", 1, args); messageChannel->Send(&msg); // XXX wait for response msg.WaitForResponse(); // XXX optional? @@ -129,10 +141,18 @@ static void node_php_embed_register_server_variables(zval *track_vars_array TSRM { // Fetch the ExecutionStream object for this thread. AsyncMessageWorker::MessageChannel *messageChannel = NODE_PHP_EMBED_G(messageChannel); + PhpRequestWorker *worker = (PhpRequestWorker *) + (messageChannel->GetWorker()); php_import_environment_variables(track_vars_array TSRMLS_CC); - // XXX put PHP-wrapped version of node context object in the - // $SERVER variable. - 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(), track_vars_array TSRMLS_CC); + // XXX call a JS function passing in $_SERVER to allow init? } NAN_METHOD(setIniPath) { @@ -144,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"); @@ -153,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, *source)); + Nan::AsyncQueueWorker(new PhpRequestWorker(callback, stream, context, *source)); } /** PHP module housekeeping */ diff --git a/src/node_php_jsobject_class.cc b/src/node_php_jsobject_class.cc index 8603f18..369089d 100644 --- a/src/node_php_jsobject_class.cc +++ b/src/node_php_jsobject_class.cc @@ -28,7 +28,7 @@ class JsHasPropertyMsg : public MessageToJs { Value member_; int has_set_exists_; public: - JsHasPropertyMsg(ObjectMapper *m, uint32_t objId, zval *member, int has_set_exists) + 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); @@ -105,7 +105,7 @@ class JsReadPropertyMsg : public MessageToJs { Value member_; int type_; public: - JsReadPropertyMsg(ObjectMapper* m, uint32_t objId, zval *member, int type) + JsReadPropertyMsg(ObjectMapper* m, objid_t objId, zval *member, int type) : MessageToJs(m), object_(), member_(m, member), type_(type) { object_.SetJsObject(objId); } @@ -130,7 +130,7 @@ class JsReadPropertyMsg : public MessageToJs { }; static zval *node_php_jsobject_read_property(zval *object, zval *member, int type ZEND_HASH_KEY_DC TSRMLS_DC) { - ZVal retval; + ZVal retval(ZEND_FILE_LINE_C); if (Z_TYPE_P(member) != IS_STRING) { return retval.Escape(); } @@ -141,7 +141,7 @@ static zval *node_php_jsobject_read_property(zval *object, zval *member, int typ msg.WaitForResponse(); // ok, result is in msg.retval_ or msg.exception_ if (msg.HasException()) { msg.retval_.SetNull(); /* sigh */ } - msg.retval_.ToPhp(obj->channel, *retval TSRMLS_CC); + msg.retval_.ToPhp(obj->channel, retval TSRMLS_CC); return retval.Escape(); } @@ -168,6 +168,9 @@ static void node_php_jsobject_free_storage(void *object, zend_object_handle hand // 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); } @@ -186,14 +189,14 @@ static zend_object_value node_php_jsobject_new(zend_class_entry *ce TSRMLS_DC) { return retval; } -void node_php_embed::node_php_jsobject_create(zval *res, ObjectMapper *mapper, uint32_t id TSRMLS_DC) { +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->channel = static_cast(mapper); + c->channel = channel; c->id = id; #if 0 c->flags = flags; diff --git a/src/node_php_jsobject_class.h b/src/node_php_jsobject_class.h index 408af4b..19bc1c9 100644 --- a/src/node_php_jsobject_class.h +++ b/src/node_php_jsobject_class.h @@ -7,19 +7,21 @@ extern "C" { #include "Zend/zend.h" } +#include "values.h" /* for objid_t */ + namespace node_php_embed { class JsMessageChannel; -class ObjectMapper; struct node_php_jsobject { zend_object std; JsMessageChannel *channel; - uint32_t id; + objid_t id; }; -/* Create a PHP proxy for a JS object */ -void node_php_jsobject_create(zval *res, ObjectMapper *mapper, uint32_t id TSRMLS_DC); +/* 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); } diff --git a/src/values.h b/src/values.h index b4a0cb3..8c46d47 100644 --- a/src/values.h +++ b/src/values.h @@ -18,21 +18,50 @@ class NonAssignable { NonAssignable() {} }; +typedef uint32_t objid_t; + class ObjectMapper { public: - virtual uint32_t IdForJsObj(const v8::Local o) = 0; - virtual uint32_t IdForPhpObj(const zval *o) = 0; - virtual void GetPhp(uint32_t id, zval *return_value TSRMLS_DC) = 0; - virtual v8::Local GetJs(uint32_t id) = 0; + // 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() { ALLOC_INIT_ZVAL(zvalp); } - virtual ~ZVal() { zval_ptr_dtor(&zvalp); } - inline zval * operator*() const { return zvalp; } + 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() { 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; } @@ -42,13 +71,26 @@ class ZVal : public NonAssignable { 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); } + 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 @@ -60,7 +102,7 @@ class ZVal : public NonAssignable { explicit Base() { } virtual ~Base() { } virtual v8::Local ToJs(ObjectMapper *m) const = 0; - virtual void ToPhp(ObjectMapper *m, zval *return_value TSRMLS_DC) const = 0; + virtual void ToPhp(ObjectMapper *m, zval *return_value, zval **return_value_ptr TSRMLS_DC) const = 0; }; class Null : public Base { public: @@ -69,7 +111,7 @@ class ZVal : public NonAssignable { Nan::EscapableHandleScope scope; return scope.Escape(Nan::Null()); } - virtual void ToPhp(ObjectMapper *m, zval *return_value TSRMLS_DC) const { + virtual void ToPhp(ObjectMapper *m, zval *return_value, zval **return_value_ptr TSRMLS_DC) const { RETURN_NULL(); } }; @@ -81,7 +123,7 @@ class ZVal : public NonAssignable { Nan::EscapableHandleScope scope; return scope.Escape(Nan::New(value_)); } - virtual void ToPhp(ObjectMapper *m, zval *return_value TSRMLS_DC) const { + virtual void ToPhp(ObjectMapper *m, zval *return_value, zval **return_value_ptr TSRMLS_DC) const { RETURN_BOOL(value_); } }; @@ -99,8 +141,12 @@ class ZVal : public NonAssignable { } return scope.Escape(Nan::New((double)value_)); } - virtual void ToPhp(ObjectMapper *m, zval *return_value TSRMLS_DC) const { - RETURN_LONG((long)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 { @@ -111,7 +157,7 @@ class ZVal : public NonAssignable { Nan::EscapableHandleScope scope; return scope.Escape(Nan::New(value_)); } - virtual void ToPhp(ObjectMapper *m, zval *return_value TSRMLS_DC) const { + virtual void ToPhp(ObjectMapper *m, zval *return_value, zval **return_value_ptr TSRMLS_DC) const { RETURN_DOUBLE(value_); } }; @@ -126,7 +172,7 @@ class ZVal : public NonAssignable { Nan::EscapableHandleScope scope; return scope.Escape(Nan::New(data_, length_).ToLocalChecked()); } - virtual void ToPhp(ObjectMapper *m, zval *return_value TSRMLS_DC) const { + virtual void ToPhp(ObjectMapper *m, zval *return_value, zval **return_value_ptr TSRMLS_DC) const { RETURN_STRINGL(data_, length_, 1); } }; @@ -140,42 +186,34 @@ class ZVal : public NonAssignable { efree(const_cast(data_)); } } - virtual void ToPhp(ObjectMapper *m, zval *return_value TSRMLS_DC) const { - RETVAL_STRINGL(data_, length_, 0/*no dup required*/); - //data_ = NULL; - } }; - class JsObj : public Base { - uint32_t id_; + class Obj : public Base { + objid_t id_; public: - explicit JsObj(ObjectMapper *m, v8::Local o) - : id_(m->IdForJsObj(o)) { } - explicit JsObj(uint32_t id) - : id_(id) { } + explicit Obj(objid_t id) : id_(id) { } virtual v8::Local ToJs(ObjectMapper *m) const { Nan::EscapableHandleScope scope; - return scope.Escape(m->GetJs(id_)); + return scope.Escape(m->JsObjForId(id_)); } - virtual void ToPhp(ObjectMapper *m, zval *return_value TSRMLS_DC) const { - // wrap js object - node_php_jsobject_create(return_value, m, id_ TSRMLS_CC); + 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 PhpObj : public Base { - uint32_t id_; + 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) - : id_(m->IdForPhpObj(o)) { } - explicit PhpObj(uint32_t id) - : id_(id) { } - virtual v8::Local ToJs(ObjectMapper *m) const { - Nan::EscapableHandleScope scope; - // XXX wrap php object - return scope.Escape(Nan::Null()); - } - virtual void ToPhp(ObjectMapper *m, zval *return_value TSRMLS_DC) const { - m->GetPhp(id_, return_value TSRMLS_CC); - } + : Obj(m->IdForPhpObj(o)) { } + explicit PhpObj(objid_t id) : Obj(id) { } }; public: @@ -294,7 +332,7 @@ class ZVal : public NonAssignable { void SetJsObject(ObjectMapper *m, v8::Local o) { SetJsObject(m->IdForJsObj(o)); } - void SetJsObject(uint32_t id) { + void SetJsObject(objid_t id) { PerhapsDestroy(); type_ = VALUE_JSOBJ; new (&jsobj_) JsObj(id); @@ -302,7 +340,7 @@ class ZVal : public NonAssignable { void SetPhpObject(ObjectMapper *m, zval *o) { SetPhpObject(m->IdForPhpObj(o)); } - void SetPhpObject(uint32_t id) { + void SetPhpObject(objid_t id) { PerhapsDestroy(); type_ = VALUE_PHPOBJ; new (&phpobj_) PhpObj(id); @@ -312,8 +350,13 @@ class ZVal : public NonAssignable { // should we create a new escapablehandlescope here? return AsBase().ToJs(m); } - void ToPhp(ObjectMapper *m, zval *return_value TSRMLS_DC) { - AsBase().ToPhp(m, return_value TSRMLS_CC); + // 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); diff --git a/test/context.js b/test/context.js new file mode 100644 index 0000000..646ea93 --- /dev/null +++ b/test/context.js @@ -0,0 +1,51 @@ +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 } + } + }).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' + ); + }); + }); +}); diff --git a/test/context.php b/test/context.php new file mode 100644 index 0000000..5c71a87 --- /dev/null +++ b/test/context.php @@ -0,0 +1,10 @@ +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); +?> From 8fabf27673c8566f29b4d55cb429280b89e3e767 Mon Sep 17 00:00:00 2001 From: "C. Scott Ananian" Date: Mon, 19 Oct 2015 02:00:49 -0400 Subject: [PATCH 03/10] Add "Buffer" type to allow passing PHP output as buffers to Node. --- src/messages.h | 3 +++ src/node_php_embed.cc | 8 ++++---- src/values.h | 39 +++++++++++++++++++++++++++++++++++++-- test/context.js | 8 ++++++-- test/context.php | 2 ++ 5 files changed, 52 insertions(+), 8 deletions(-) diff --git a/src/messages.h b/src/messages.h index 33444aa..8d3ac61 100644 --- a/src/messages.h +++ b/src/messages.h @@ -100,6 +100,9 @@ class JsInvokeMethodMsg : public MessageToJs { 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)); diff --git a/src/node_php_embed.cc b/src/node_php_embed.cc index 0941cd2..6a58b1a 100644 --- a/src/node_php_embed.cc +++ b/src/node_php_embed.cc @@ -117,12 +117,12 @@ static int node_php_embed_ub_write(const char *str, unsigned int str_length TSRM (messageChannel->GetWorker()); ZVal stream(ZEND_FILE_LINE_C); worker->GetStream().ToPhp(messageChannel, stream TSRMLS_CC); - // Creating buf "the hard way" to avoid unnecessary copying of str. - zval buf; - INIT_ZVAL(buf); ZVAL_STRINGL(&buf, str, str_length, 0); + zval buf; INIT_ZVAL(buf); // stack allocate a null zval as a placeholder zval *args[] = { &buf }; - // XXX, would be better to pass this as a buffer, not a string. 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? diff --git a/src/values.h b/src/values.h index 8c46d47..273739d 100644 --- a/src/values.h +++ b/src/values.h @@ -187,6 +187,22 @@ class ZVal : public NonAssignable { } } }; + 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: @@ -252,6 +268,9 @@ class ZVal : public NonAssignable { 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; @@ -329,6 +348,16 @@ class ZVal : public NonAssignable { 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)); } @@ -380,11 +409,13 @@ class ZVal : public NonAssignable { } enum ValueTypes { VALUE_EMPTY, VALUE_NULL, VALUE_BOOL, VALUE_INT, VALUE_DOUBLE, - VALUE_STR, VALUE_OSTR, VALUE_JSOBJ, VALUE_PHPOBJ + 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_; JsObj jsobj_; PhpObj phpobj_; + Str str_; OStr ostr_; Buf buf_; OBuf obuf_; + JsObj jsobj_; PhpObj phpobj_; }; const Base &AsBase() { switch(type_) { @@ -402,6 +433,10 @@ class ZVal : public NonAssignable { 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: diff --git a/test/context.js b/test/context.js index 646ea93..5772b9d 100644 --- a/test/context.js +++ b/test/context.js @@ -33,7 +33,9 @@ describe('Passing context object from JS to PHP', function() { d: (((1<<30)-1)*4), e: 1.5, f: 'abcdef \uD83D\uDCA9', - g: { f: 1 } + g: { f: 1 }, + h: function fname(x) { return x; }, + i: new Buffer('abc', 'utf8') } }).then(function(v) { out.toString().replace(/int\(4294967292\)/,'float(4294967292)') @@ -44,7 +46,9 @@ describe('Passing context object from JS to PHP', function() { 'float(4294967292)\n' + 'float(1.5)\n' + 'string(11) "abcdef \uD83D\uDCA9"\n' + - 'int(1)\n' + 'int(1)\n' + + 'string(5) "fname"\n' + + 'string(3) "abc"\n' ); }); }); diff --git a/test/context.php b/test/context.php index 5c71a87..b1fc4d1 100644 --- a/test/context.php +++ b/test/context.php @@ -7,4 +7,6 @@ var_dump($c->e); var_dump($c->f); var_dump($c->g->f); +var_dump($c->h->name); +var_dump($c->i); ?> From 304000e2b9e284aabedebcf46347c6cc030416e8 Mon Sep 17 00:00:00 2001 From: "C. Scott Ananian" Date: Mon, 19 Oct 2015 17:28:08 -0400 Subject: [PATCH 04/10] Fix memory errors in read_property. Use the __get magic method, instead of attempting to implement the read_property handler, since the expected refcount properties of the value returned from the read_property handler seem to be exceedingly baroque. --- src/asyncmessageworker.h | 17 ++++---- src/messages.h | 2 +- src/node_php_embed.cc | 8 ++-- src/node_php_jsobject_class.cc | 77 ++++++++++++++++++++++++++++------ src/values.h | 18 ++++++-- test/context.js | 67 +++++++++++++++++++++++++++++ test/context2.php | 46 ++++++++++++++++++++ 7 files changed, 208 insertions(+), 27 deletions(-) create mode 100644 test/context2.php diff --git a/src/asyncmessageworker.h b/src/asyncmessageworker.h index d6e9ca5..0738a67 100644 --- a/src/asyncmessageworker.h +++ b/src/asyncmessageworker.h @@ -63,16 +63,19 @@ namespace node_php_embed { } // Map PHP object to an index (PHP thread only) objid_t IdForPhpObj(zval *z) { - if (phpObjToId_.count(z)) { - return phpObjToId_.at(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_); - Z_ADDREF_P(z); if (id >= phpObjList_.size()) { phpObjList_.resize(id+1); } + // xxx clone/separate z? + Z_ADDREF_P(z); phpObjList_[id] = z; - phpObjToId_[z] = id; + phpObjToId_[handle] = id; return id; } // returned value is owned by objectmapper, caller should not release it. @@ -82,7 +85,7 @@ namespace node_php_embed { if (z.IsNull()) { node_php_jsobject_create(z.Ptr(), channel, id TSRMLS_CC); phpObjList_[id] = z.Ptr(); - phpObjToId_[z.Ptr()] = id; + phpObjToId_[Z_OBJ_HANDLE_P(z.Ptr())] = id; // one excess reference: owned by objectmapper return z.Escape(); } @@ -94,7 +97,7 @@ namespace node_php_embed { zval *z = (id < phpObjList_.size()) ? phpObjList_[id] : NULL; if (z) { phpObjList_[id] = NULL; - phpObjToId_.erase(z); + phpObjToId_.erase(Z_OBJ_HANDLE_P(z)); zval_ptr_dtor(&z); } } @@ -255,7 +258,7 @@ namespace node_php_embed { Nan::Persistent jsObjToId_; // PHP Object mapping // Read/writable only from PHP thread - std::unordered_map phpObjToId_; + std::unordered_map phpObjToId_; std::vector phpObjList_; // Ids are allocated from both threads, so mutex is required uv_mutex_t id_lock_; diff --git a/src/messages.h b/src/messages.h index 8d3ac61..78f0375 100644 --- a/src/messages.h +++ b/src/messages.h @@ -140,7 +140,7 @@ class PhpGetPropertyMsg : public MessageToPhp { : 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 obj{ZEND_FILE_LINE_C}, name{ZEND_FILE_LINE_C}; zval *r; zend_class_entry *ce; zend_property_info *property_info; diff --git a/src/node_php_embed.cc b/src/node_php_embed.cc index 6a58b1a..1646f29 100644 --- a/src/node_php_embed.cc +++ b/src/node_php_embed.cc @@ -63,7 +63,7 @@ class node_php_embed::PhpRequestWorker : public AsyncMessageWorker { } NODE_PHP_EMBED_G(messageChannel) = &messageChannel; { - ZVal retval(ZEND_FILE_LINE_C); + ZVal retval{ZEND_FILE_LINE_C}; zend_first_try { char eval_msg[] = { "request" }; // shows up in error messages if (FAILURE == zend_eval_string_ex(source_, *retval, eval_msg, true TSRMLS_CC)) { @@ -115,7 +115,7 @@ static int node_php_embed_ub_write(const char *str, unsigned int str_length TSRM AsyncMessageWorker::MessageChannel *messageChannel = NODE_PHP_EMBED_G(messageChannel); PhpRequestWorker *worker = (PhpRequestWorker *) (messageChannel->GetWorker()); - ZVal stream(ZEND_FILE_LINE_C); + 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 }; @@ -148,10 +148,10 @@ static void node_php_embed_register_server_variables(zval *track_vars_array TSRM // relative to the document root." // XXX // Put PHP-wrapped version of node context object in $_SERVER['CONTEXT'] - ZVal context(ZEND_FILE_LINE_C); + ZVal context{ZEND_FILE_LINE_C}; worker->GetContext().ToPhp(messageChannel, context TSRMLS_CC); char contextName[] = { "CONTEXT" }; - php_register_variable_ex(contextName, context.Transfer(), track_vars_array TSRMLS_CC); + 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? } diff --git a/src/node_php_jsobject_class.cc b/src/node_php_jsobject_class.cc index 369089d..ab1e462 100644 --- a/src/node_php_jsobject_class.cc +++ b/src/node_php_jsobject_class.cc @@ -13,6 +13,8 @@ extern "C" { #include "values.h" #include "macros.h" +#define USE_MAGIC_ISSET 0 + using namespace node_php_embed; /* Class Entries */ @@ -81,6 +83,36 @@ class JsHasPropertyMsg : public MessageToJs { } }; +#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() @@ -99,6 +131,7 @@ static int node_php_jsobject_has_property(zval *object, zval *member, int has_se if (msg.HasException()) { return false; /* sigh */ } return msg.retval_.AsBool(); } +#endif /* USE_MAGIC_ISSET */ class JsReadPropertyMsg : public MessageToJs { Value object_; @@ -129,23 +162,30 @@ class JsReadPropertyMsg : public MessageToJs { } }; -static zval *node_php_jsobject_read_property(zval *object, zval *member, int type ZEND_HASH_KEY_DC TSRMLS_DC) { - ZVal retval(ZEND_FILE_LINE_C); - if (Z_TYPE_P(member) != IS_STRING) { - return retval.Escape(); + +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(object TSRMLS_CC); - JsReadPropertyMsg msg(obj->channel, obj->id, member, type); + 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()) { msg.retval_.SetNull(); /* sigh */ } - msg.retval_.ToPhp(obj->channel, retval TSRMLS_CC); - return retval.Escape(); + 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; @@ -224,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 }; @@ -245,8 +296,10 @@ 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; diff --git a/src/values.h b/src/values.h index 273739d..2fcdb02 100644 --- a/src/values.h +++ b/src/values.h @@ -60,7 +60,14 @@ class ZVal : public NonAssignable { // 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() { transferred=true; return zvalp; } + 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; } @@ -180,10 +187,15 @@ class ZVal : public NonAssignable { // an "owned string", will copy data on creation and free it on delete. public: explicit OStr(const char *data, std::size_t length) - : Str(estrndup(data, length+1), length) { } + : Str(NULL, length) { + char *ndata = new char[length+1]; + memcpy(ndata, data, length); + ndata[length] = 0; + data_ = ndata; + } virtual ~OStr() { if (data_) { - efree(const_cast(data_)); + delete[] data_; } } }; diff --git a/test/context.js b/test/context.js index 5772b9d..c886ef9 100644 --- a/test/context.js +++ b/test/context.js @@ -52,4 +52,71 @@ describe('Passing context object from JS to PHP', function() { ); }); }); + 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/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"; + +?> From a708655d2e00b5534ad518488adfc94fcadafc8f Mon Sep 17 00:00:00 2001 From: "C. Scott Ananian" Date: Mon, 19 Oct 2015 23:31:17 -0400 Subject: [PATCH 05/10] Add segfault handler for debugging crashes. --- package.json | 5 +++-- test-setup.js | 2 ++ test/mocha.opts | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 test-setup.js diff --git a/package.json b/package.json index 74e1fd2..4ec49f7 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,8 @@ ], "devDependencies": { "mocha": "~2.3.3", - "should": "~7.1.0", - "readable-stream": "~2.0.2" + "readable-stream": "~2.0.2", + "segfault-handler": "~1.0.0", + "should": "~7.1.0" } } diff --git a/test-setup.js b/test-setup.js new file mode 100644 index 0000000..473c9bc --- /dev/null +++ b/test-setup.js @@ -0,0 +1,2 @@ +var SegfaultHandler = require('segfault-handler'); +SegfaultHandler.registerHandler("segfault.log"); 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 From 77b711e074bcad86d6e27a0294d862b6e71303b5 Mon Sep 17 00:00:00 2001 From: "C. Scott Ananian" Date: Tue, 20 Oct 2015 12:02:15 -0400 Subject: [PATCH 06/10] Listen for SIGABRT in addition to SIGSEGV. Our OSX builds seem to crash with SIGABRT. --- package.json | 2 +- test-setup.js | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 4ec49f7..8cc69c0 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "devDependencies": { "mocha": "~2.3.3", "readable-stream": "~2.0.2", - "segfault-handler": "~1.0.0", + "segfault-handler": "git+https://github.com/cscott/node-segfault-handler#any-signal", "should": "~7.1.0" } } diff --git a/test-setup.js b/test-setup.js index 473c9bc..67a91f8 100644 --- a/test-setup.js +++ b/test-setup.js @@ -1,2 +1,5 @@ var SegfaultHandler = require('segfault-handler'); +// Listen for SIGSEGV. SegfaultHandler.registerHandler("segfault.log"); +// Listen for SIGABRT, too. +SegfaultHandler.registerHandler(SegfaultHandler.SIGABRT); From e420aad38e39b89a2c53202d6acbb1badda4869a Mon Sep 17 00:00:00 2001 From: "C. Scott Ananian" Date: Tue, 20 Oct 2015 00:11:32 -0400 Subject: [PATCH 07/10] Fix startup crash. --- src/node_php_embed.cc | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/node_php_embed.cc b/src/node_php_embed.cc index 1646f29..aeaf630 100644 --- a/src/node_php_embed.cc +++ b/src/node_php_embed.cc @@ -224,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 From d4ca314cd485ec725fbcf750fe7fa4acb4308d28 Mon Sep 17 00:00:00 2001 From: "C. Scott Ananian" Date: Mon, 19 Oct 2015 23:52:37 -0400 Subject: [PATCH 08/10] Limit supported configurations to node >= 1.8.4. We use C++11 features, which older versions of node do not support. --- .travis.yml | 17 +---------------- package.json | 1 + 2 files changed, 2 insertions(+), 16 deletions(-) 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/package.json b/package.json index 8cc69c0..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", From 4006cd7eb643a90daeb9ff96a96c6abf90addcbe Mon Sep 17 00:00:00 2001 From: "C. Scott Ananian" Date: Mon, 19 Oct 2015 23:00:07 -0400 Subject: [PATCH 09/10] Allow using a Map instead of a WeakMap for older node versions. --- src/asyncmessageworker.h | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/asyncmessageworker.h b/src/asyncmessageworker.h index 0738a67..a415460 100644 --- a/src/asyncmessageworker.h +++ b/src/asyncmessageworker.h @@ -7,6 +7,17 @@ #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 @@ -44,13 +55,13 @@ namespace node_php_embed { // Have we already mapped this? Nan::HandleScope scope; v8::Local jsObjToId = Nan::New(jsObjToId_); - if (jsObjToId->Has(o)) { - return Nan::To(jsObjToId->Get(o)).FromJust(); + 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(o, Nan::New(id)); + jsObjToId->Set(WEAKMAP_CTXT o, Nan::New(id)); SaveToPersistent(id, o); return id; } @@ -115,7 +126,7 @@ namespace node_php_embed { // 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(o.ToLocalChecked()); + jsObjToId->Delete(WEAKMAP_CTXT o.ToLocalChecked()); SaveToPersistent(id, Nan::Undefined()); } void ClearAllJsIds() { From 675dcd4e6602be6572f2c55a894de5684ce3b2d5 Mon Sep 17 00:00:00 2001 From: "C. Scott Ananian" Date: Tue, 20 Oct 2015 12:08:41 -0400 Subject: [PATCH 10/10] WIP: node 1.8.4 support Turns out that using Map instead of NativeWeakMap isn't a solution for node 1.8.4, since it doesn't export Map either. Sigh. We can hack up our own NativeWeakMap using hidden properties, as shown by this WIP patch, but I don't think it's worth it. Let's just not support node <= 1.8.4. --- src/poorweakmap.h | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/poorweakmap.h 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_; + }