Skip to content

Commit

Permalink
Add Js\ByRef() class to allow values to be passed by reference to JS.
Browse files Browse the repository at this point in the history
  • Loading branch information
cscott committed Nov 3, 2015
1 parent 4f5a23c commit af804b5
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 32 deletions.
11 changes: 11 additions & 0 deletions lib/startup.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,15 @@
// helpers which are easier to write in PHP than in C.
namespace Js;

// Simple marker class to indicate that a given value should be
// passed by reference. Internally we'll call `getValue` to
// unwrap the reference inside.
class ByRef {
public $value;
public function __construct(&$value) { $this->value =& $value; }
public function &getValue() {
return $this->value;
}
}

?>
12 changes: 7 additions & 5 deletions src/node_php_jsobject_class.cc
Original file line number Diff line number Diff line change
Expand Up @@ -359,10 +359,14 @@ class JsInvokeMsg : public MessageToJs {
objid_t objId, zval *member, ulong argc, zval **argv TSRMLS_DC)
: MessageToJs(m, callback, isSync),
object_(), member_(m, member TSRMLS_CC),
argc_(argc), argv_(Value::NewArray(m, argc, argv TSRMLS_CC)) {
argc_(argc), argv_() {
object_.SetJsObject(objId);
argv_.SetArrayByValue(argc, [m, argv TSRMLS_CC](uint32_t idx, Value& v) {
ZVal z(argv[idx] ZEND_FILE_LINE_CC);
z.UnwrapByRef(TSRMLS_C); // Unwrap Js\ByRef values.
v.Set(m, z TSRMLS_CC);
});
}
~JsInvokeMsg() override { delete[] argv_; }

protected:
void InJs(JsObjectMapper *m) override {
Expand Down Expand Up @@ -431,7 +435,7 @@ class JsInvokeMsg : public MessageToJs {
Value object_;
Value member_;
ulong argc_;
Value *argv_;
Value argv_;
};

// XXX Figure out how to actually invoke methods async.
Expand All @@ -442,8 +446,6 @@ ZEND_BEGIN_ARG_INFO_EX(node_php_jsobject_call_args, 0, 1/*return by ref*/, 1)
ZEND_ARG_ARRAY_INFO(0, args, 0)
ZEND_END_ARG_INFO()

// XXX can't figure out how to pass arrays by reference instead of
// by value.
PHP_METHOD(JsObject, __call) {
zval *member; zval *args;
TRACE(">");
Expand Down
23 changes: 23 additions & 0 deletions src/values.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

extern "C" {
#include "main/php.h"
#include "Zend/zend_interfaces.h" // for zend_call_method_with_*
}

#include "src/macros.h"
Expand Down Expand Up @@ -101,6 +102,24 @@ class ZVal {
return z;
}

// Unwrap references protected by Js\ByRef
inline void UnwrapByRef(TSRMLS_D) {
if (!IsObject()) { return; }
assert(zvalp && !transferred_);
zend_class_entry *ce = Z_OBJCE_P(zvalp);
// XXX cache the zend_class_entry at startup so we can do a simple
// pointer comparison instead of looking at the class name
if (ce->name_length == 8 && strcmp("Js\\ByRef", ce->name) == 0) {
// Unwrap!
zval *rv;
zend_call_method_with_0_params(&zvalp, nullptr, nullptr, "getValue", &rv);
if (rv) {
zval_ptr_dtor(&zvalp);
zvalp = rv;
}
}
}

// Support a PHP calling convention where the actual zval object
// is owned by the caller, but the contents are transferred to the
// callee.
Expand Down Expand Up @@ -694,6 +713,10 @@ class Value {
inline bool IsArrayByValue() const {
return (type_ == VALUE_ARRAY_BY_VALUE);
}
inline Value& operator[](int i) const {
assert(IsArrayByValue());
return array_by_value_.item_[i];
}
bool AsBool() const {
switch (type_) {
case VALUE_BOOL:
Expand Down
48 changes: 48 additions & 0 deletions test/byref.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Test cases for passing values "by reference".
var StringStream = require('../test-stream.js');

var path = require('path');
var should = require('should');

describe('Wrapped PHP objects passed by reference to JavaScript', function() {
var php = require('../');
var defaultCode = [
'call_user_func(function () {',
' $ctxt = $_SERVER["CONTEXT"];',
' #include $ctxt->file;',
'',
' $a = {{VALUE}};',
' $b = $a;',
' $ctxt->jsfunc(new Js\\ByRef($a));',
' var_dump($a);',
' var_dump($b);',
'})',
].join('\n');
var test = function(value, f) {
var code = defaultCode.replace('{{VALUE}}', value);
var out = new StringStream();
return php.request({ source: code, context: {
jsfunc: f,
file: path.join(__dirname, 'byref.php'),
}, stream: out, }).then(function(v) { return [v, out.toString()]; });
};
it('arrays', function() {
return test('array("a"=>"b")',function(a, b) {
a[1] = 2;
}).spread(function(v, out) {
out.should.equal([
'array(2) {',
' ["a"]=>',
' string(1) "b"',
' [1]=>',
' int(2)',
'}',
'array(1) {',
' ["a"]=>',
' string(1) "b"',
'}',
'',
].join('\n'));
});
});
});
30 changes: 3 additions & 27 deletions test/phparray.js
Original file line number Diff line number Diff line change
Expand Up @@ -535,38 +535,14 @@ describe('Wrapped PHP arrays accessed from JavaScript', function() {
});
it('can be passed by reference', function() {
return test(function(arr) {
arr.offsetSet(1, 2);
arr.offsetSet('foo', 'bar');
// XXX make the below work.
// `arr[1] = 2;`
// `arr['foo'] = 'bar';`
arr[1] = 2;
arr.set('foo', 'bar');
}, [
'call_user_func(function() {',
// XXX this `wrap` class should be built-in as Js\ArrayByRef
' class wrap implements ArrayAccess, Countable {',
' private $a;',
' public function __construct(&$a) { $this->a =& $a; }',
' public function offsetSet($offset, $value) {',
' if (is_null($offset)) { $this->a[] = $value; }',
' else { $this->a[$offset] = $value; }',
' }',
' public function offsetExists($offset) {',
' return isset($this->a[$offset]);',
' }',
' public function offsetUnset($offset) {',
' unset($this->a[$offset]);',
' }',
' public function offsetGet($offset) {',
' return isset($this->a[$offset]) ? $this->a[$offset] : null;',
' }',
' public function count() {',
' return count($this->a);',
' }',
' }',
' $a = array("a" => "b");',
' $b = $a;',
' $ctxt = $_SERVER["CONTEXT"];',
' $ctxt->jsfunc(new wrap($a));',
' $ctxt->jsfunc(new Js\\ByRef($a));',
' var_dump($a);',
' var_dump($b);',
'})',
Expand Down

0 comments on commit af804b5

Please sign in to comment.