Character width bug in Bitvise SSH Client

1. Bug encountered

Some time ago, I switched to ZSH, using oh-my-zsh‘s default theme, robbyrussell. On my very first attempt to use it, I discovered a mysterious bug. For example:

When auto-completing with TAB, the current line will shift back one character. It’s harmless in this occasion, however, if you are auto-completing in the middle of a long command line, this will probably mess thing up, and you have to rewrite the entire command.

After that, I tested this in GNOME terminal, and it worked fine.

2. Bug located

I looked through the GitHub issue page, and found out that no one else seem to have encountered a similar issue. Everyone uses auto-complete, if there’s really a bug, how come it’s never mentioned? Perhaps there’s a bug in the terminal emulator of the SSH client I was using.

The arrow character at the beginning of the line seems larger in Bitvise xterm than that in GNOME terminal, so I highlighted them and made a comparison.

Now we see the difference. In Bitvise xterm the arrow character is displayed two characters wide. In GNOME terminal, it’s the same width as ASCII characters. The difference in display was probably due to the two terminals using different fonts. However, this is not the cause of the bug, either.

When we echo '➜' | xxd we can see that the UTF-8 encoded right arrow is 0xE29E9C. Now we vim ~/.oh-my-zsh/themes/robbyrussell.zsh-theme and replace arrows with a Chinese character “哈”(UTF-8 0xE59388), open a new terminal, and try again.

Auto-complete behavior with the Chinese character is normal, while the arrow character(having the same size and display width) isn’t. Why? Now type an arrow into the terminal, and try to delete it with backspace.

Uh-oh, the cursor only shifted back one character, and the arrow is still there. Now we found the bug. Bitvise xterm treated the arrow character as one character wide, but display it with two characters wide.

In our first case, tab auto-completing will relocate the line using an offset depending on how many characters are before it. The arrow character is treated as one character wide, so the offset is 5 instead of 6. That’s why the command line is shifted back one character.

3. Workaround

Workaround to this bug is simple. Just prevent using such characters in command line. To stop auto-completing from going wrong, change the theme configuration file and replace the arrow with something else. For example, I changed it into %(!.#.$).

Now the problem is solved.

PHP Retval: Used or Not?

1. Deducing whether return value will be used

When we call a PHP function, it may return a value. However, we may not always use it. In this case, whatever the function does for returning that value is a waste of CPU cycles. In some rare cases, to improve performance, we may want to deduce whether the return value will be used.

Unfortunately, there’s no way to do that in userland PHP, but in a PHP extension, that’s quite easy. There’s a macro defined in zend.h.

1
2
3
4
#define USED_RET() \
(!EX(prev_execute_data) || \
!ZEND_USER_CODE(EX(prev_execute_data)->func->common.type) || \
(EX(prev_execute_data)->opline->result_type != IS_UNUSED))

We can see that the return value is considered used if at least one of the following conditions is met.

  • There’s no previous execute data.
  • The function which called this function is not defined in userland PHP.
  • The result type stored in the opline of previous execute data is not IS_UNUSED.

1.1 No previous execute data?

Well, the global scope has no previous execute data. You can return from global scope so that another script who includes this script will get the return value. For example:

1
2
3
// foo.php
<?php
return 'foo';
1
2
3
// bar.php
<?php
$bar = include 'foo.php';

Invoking USED_RET() in global scope will always get true. However, that doesn’t concern us, because the native functions we implement in a PHP extension is never in global scope.

1.2 Not userland PHP?

The func property of previous execute data stores the pointer to the zend_function from which the current function is being invoked. For example, we define two native functions in an extension like this:

1
2
3
4
5
6
7
8
9
10
11
12
PHP_FUNCTION(foo)
{
RETVAL_LONG(EX(prev_execute_data)->func->common.type);
}
PHP_FUNCTION(bar)
{
zval retval, foo;
ZVAL_STRING(&foo, "foo");
call_user_function(EX(function_table), NULL, &foo, &retval, 0, NULL);
zval_ptr_dtor(&foo);
RETVAL_LONG(Z_LVAL(retval));
}

Then, run the following script, and you will get “2 4 1” as output.

1
2
3
4
$type_a = foo();
$type_b = eval('return foo();');
$type_c = bar();
echo "$type_a $type_b $type_c", PHP_EOL;

Let’s take a look at zend_compile.c:

1
2
3
4
5
6
7
8
#define ZEND_INTERNAL_FUNCTION 1
#define ZEND_USER_FUNCTION 2
#define ZEND_OVERLOADED_FUNCTION 3
#define ZEND_EVAL_CODE 4
#define ZEND_OVERLOADED_FUNCTION_TEMPORARY 5
/* A quick check (type == ZEND_USER_FUNCTION || type == ZEND_EVAL_CODE) */
#define ZEND_USER_CODE(type) ((type & 1) == 0)

So it’s clear that if the previous scope is in a function defined in PHP, the type property yields ZEND_USER_FUNCTION. If in evaled code, ZEND_EVAL_CODE. If in a native function, ZEND_INTERNAL_FUNCTION. The ZEND_USER_CODE() macro is used to detect whether a function type is either ZEND_USER_FUNCTION or ZEND_EVAL_CODE. If so, that function is considered userland PHP.

The USED_RET() macro will always yield true if the function which invoked the current function is not defined in userland PHP. Why? Because there’s no way to check whether a native function(like bar() in our example) will use the return value or not.

1.3 Result type of opline?

Finally, we check whether return value is used by checking the result_type property of the opline from the calling scope. You can understand opline as “line of opcode”, which is the current line of opcode executed from the calling scope.

The result_type can be one of the following values:

1
2
3
4
5
#define IS_CONST (1<<0)
#define IS_TMP_VAR (1<<1)
#define IS_VAR (1<<2)
#define IS_UNUSED (1<<3) /* Unused variable */
#define IS_CV (1<<4) /* Compiled variable */

If the value of result_type is IS_UNUSED, we are certain that the return value of the called function will not be used.

2. Usage

As said in the previous section, we can use USED_RET() when the return value of our native function is not mandatory, and may cause performance overhead. Some built-in functions of PHP already take advantage of that macro, for example, functions which manipulate the internal pointer of a zend_array, such as next(), prev(), end(), reset(). If the user just want to do something to the pointer without fetching the corresponding value of the array, the function won’t do the fetch & return job.

For example, in array.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/* {{{ proto mixed next(array array_arg)
Move array argument's internal pointer to the next element and return it */
PHP_FUNCTION(next)
{
HashTable *array;
zval *entry;
ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_ARRAY_OR_OBJECT_HT_EX(array, 0, 1)
ZEND_PARSE_PARAMETERS_END();
zend_hash_move_forward(array);
if (USED_RET()) {
if ((entry = zend_hash_get_current_data(array)) == NULL) {
RETURN_FALSE;
}
if (Z_TYPE_P(entry) == IS_INDIRECT) {
entry = Z_INDIRECT_P(entry);
}
ZVAL_DEREF(entry);
ZVAL_COPY(return_value, entry);
}
}
/* }}} */

This macro is available in all current versions of PHP 7. Feel free to use it in your extension, if necessary.

3. How to do that in userland PHP?

You can’t do that in userland PHP, not without the help of a native function. Write a function in your extension like this, and it’s ready to use:

1
2
3
4
5
6
7
8
9
10
11
PHP_FUNTION(used_ret)
{
zend_execute_data* ex = EX(prev_execute_data);
if (!ex)
RETURN_TRUE;
// Fetching the previous execute data of previous execute data.
ex = ex->prev_execute_data;
if (!ex || !ZEND_USER_CODE(ex->func->common.type))
RETURN_TRUE;
RETVAL_BOOL(ex->opline->result_type != IS_UNUSED);
}

Just that simple :) Now we can test it with a PHP script.

1
2
3
4
5
6
7
8
9
10
11
function foo()
{
if (used_ret()) {
echo 'Used.', PHP_EOL;
} else {
echo 'Not used.', PHP_EOL;
}
return 0;
}
foo();
$bar = foo();

Expected output:

Not used.
Used.

'Pedantic' Copy-on-Write Inspection in PHP 7.2

1. Copy-on-Write mechanism in PHP

PHP beginners may read something like this in books when learning the basics of PHP.

1
2
3
$foo = ['foo' => 'baz']; // ['foo' => 'baz']: refcount = 1
$bar = $foo; // ['foo' => 'baz']: refcount = 2
$bar['foo'] = 'bar'; // ['foo' => 'baz']: refcount = 1, ['foo' => 'bar']: refcount = 1

PHP arrays are refcounted, so when multiple zvals are referring to a same zend_array, it won’t be copied, however, its refcount is incremented. When any of these arrays is going to be modified, Zend Engine checks the zend_array‘s refcount. If it’s not 1, create a duplicate of the array, modify it, assign the zval to it, and decrement the original array. This is called separation.

The same is true of passing an array to a function parameter by value.

1
2
3
4
5
6
function add_foo($arr) { // ['foo' => 'baz'] : refcount = 2
$arr['foo'] = 'bar'; // ['foo' => 'baz'] : refcount = 1, ['foo' => 'bar'] : refcount = 1
// ...
}
$arr = ['foo' => 'baz']; // ['foo' => 'baz'] : refcount = 1
add_foo($arr); // ['foo' => 'baz'] : refcount = 1

However, PHP objects are treated differently. You can modify a zend_object‘s property_table without separation.

1
2
3
4
5
$arr = ['foo' => 'baz'];
$foo = new ArrayObject($arr); // obj(['foo' => 'baz']) : refcount = 1
$bar = $foo; // obj(['foo' => 'baz']) : refcount = 2
$foo[] = 'abc'; // obj(['foo' => 'baz', 'abc']) : refcount = 2
add_foo($bar); // obj(['foo' => 'bar', 'abc']) : refcount = 2

2. Messing around with arrays in PHP extensions

In PHP extensions, separation should be done manually. Careless or unexperienced PHP extension developers may unintentionally mess thing up. Consider the following code.

1
2
3
4
5
6
7
8
9
10
11
PHP_FUNCTION(add_foo_internal)
{
zend_array* arr;
ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_ARRAY(arr)
ZEND_PARSE_PARAMETERS_END();
zval bar;
ZVAL_STRING(&bar, "bar");
zend_hash_str_add(arr, "foo", sizeof "foo" - 1, &bar);
// ...
}

Looks like pretty legitimate code. However, calling this function may bring you catastrophes.

1
2
3
$foo = ['bar' => 'baz']; // ['foo' => 'baz']: refcount = 1
$bar = $foo; // ['foo' => 'baz']: refcount = 2
add_foo_internal($bar); // ['foo' => 'bar']: refcount = 2

“Hey, wait a second, add_foo_internal() behaves just like add_foo() which accepts the parameter by reference. That’s not really a bad thing, after all?”

You’re WRONG.

As a result of separation, if add_foo() accepts the parameter by reference, only $bar will become ['foo' => 'bar'], while $foo will remain the same. However, the add_foo_internal() function actually modifies the zend_array by force, thus changing the values of all the zvals referencing this zend_array, which violates the copy-on-write mechanism of PHP.

Most of the times, that is not what you want. Even if it doesn’t break your extension, it will certainly break userland PHP.

3. HT_ASSERT_RC1 macro in PHP 7.2+

Since PHP 7.2, there’s a HT_ASSERT_RC1() macro defined in zend_hash.c, which is invoked before performing any modification on a zend_array in all hashtable APIs defined in this source file.

1
2
3
4
5
6
7
8
#if ZEND_DEBUG
#define HT_ASSERT(ht, expr) \
ZEND_ASSERT((expr) || ((ht)->u.flags & HASH_FLAG_ALLOW_COW_VIOLATION))
#else
#define HT_ASSERT(ht, expr)
#endif
#define HT_ASSERT_RC1(ht) HT_ASSERT(ht, GC_REFCOUNT(ht) == 1)

This assertion ensures that whenever a zend_array is modified, its refcount must be 1. If you are using a debug build of PHP, calling add_foo_internal() will trigger assertion failure like this:

php: /opt/php-7.2.4/Zend/zend_hash.c:549: _zend_hash_add_or_update_i: Assertion `((ht)->gc.refcount == 1) || ((ht)->u.flags & (1<<6))’ failed.

UPGRADING.INTERNALS didn’t mention this new feature, but it’s one of the things I like most in PHP 7.2. The rather “pedantic” inspection of copy-on-write violation on PHP arrays is indeed helpful for PHP extension developers. It helped me found out some potentially hazardous bugs in some of my old extensions.

“All right, I know PHP arrays are refcounted, and should be separated before modifying. But in some cases, I have to modify a zend_array whose refcount isn’t 1. What should I do?”

Just add a flag in the zend_array you’d like to mess with, by invoking HT_ALLOW_COW_VIOLATION() macro.

1
2
3
4
5
#if ZEND_DEBUG
#define HT_ALLOW_COW_VIOLATION(ht) (ht)->u.flags |= HASH_FLAG_ALLOW_COW_VIOLATION
#else
#define HT_ALLOW_COW_VIOLATION(ht)
#endif

However, even if you know what you’re doing, “having to do” such a thing is usually a code smell and caused by bad design. It is recommended that you refactor the code and avoid doing so.

New column in Laravel-China community

I opened a column in the Laravel-China community, PHP Extension Cookbook. It’s not much, but if you look forward to learning PHP extension development, this column can be of help.

I’ve just posted the very first article yesterday, mainly explaining how to write config.m4 script for your extension. And there’s going to be many more.

If there’s something wrong or badly explained in the articles, feel free to email me for correction. And if you’re also adept at developing PHP extensions, you’re welcome to contribute to this column.

Have a great time hacking with PHP, y’all :)

Changes of Zend API in PHP 7.3 you should be aware of

After an unsuccessful attempt to compile my extension with the latest PHP, I discovered that you can no longer directly update GC_REFCOUNT(). As in zend_types.h, macros concerning GC refcount are defined as below:

1
2
3
4
#define GC_REFCOUNT(p) zend_gc_refcount(&(p)->gc)
#define GC_SET_REFCOUNT(p, rc) zend_gc_set_refcount(&(p)->gc, rc)
#define GC_ADDREF(p) zend_gc_addref(&(p)->gc)
#define GC_DELREF(p) zend_gc_delref(&(p)->gc)

Meanwhile, in PHP 7.2 and older versions:

1
#define GC_REFCOUNT(p) (p)->gc.refcount

Here’s a simple workaround for compatibility.

1
2
3
4
5
#if PHP_VERSION_ID < 70300
#define GC_ADDREF(p) ++GC_REFCOUNT(p)
#define GC_DELREF(p) --GC_REFCOUNT(p)
#define GC_SET_REFCOUNT(p, rc) GC_REFCOUNT(p) = rc
#endif

This change in internal API was intended to eliminate race-conditions in multi-thread applications, as mentioned in this pull request.

Other notable API changes can be found here, with which you can make your extension compatible with PHP 7.3.

Fast ZPP's Incompatibility with C++

Since PHP 7.0, a new Zend API was implemented for faster parameter parsing.

For example, if your function accepts an integer parameter foo, then the code may look like this.

1
2
3
ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_LONG(foo)
ZEND_PARSE_PARAMETERS_END();

However, if your extension is written in C++, the compiler will complain and refuse to compile.

You’ll get error message like:

1
error: invalid conversion from 'int' to 'zend_expected_type {aka _zend_expected_type}' [-fpermissive]

Confused? Let’s take a look at the macro definition, which is located in zend_API.h

1
2
3
4
5
6
7
8
9
10
11
#define ZEND_PARSE_PARAMETERS_START_EX(flags, min_num_args, max_num_args) do { \
const int _flags = (flags); \
int _min_num_args = (min_num_args); \
int _max_num_args = (max_num_args); \
int _num_args = EX_NUM_ARGS(); \
int _i; \
zval *_real_arg, *_arg = NULL; \
zend_expected_type _expected_type = IS_UNDEF; \
char *_error = NULL; \
zend_bool _dummy; \
// Some more code...

We can see on line 8 Zend’s trying to initialize an enum zend_expected_type with value 0, which is forbidden in C++. In C++, you should either explicitly cast it with static_cast or initialize using a corresponding enum value.

Fortunately the value 0 is defined in macro IS_UNDEF (why this??), you can just redefine it instead of sed zend_API.h in your config.m4 script.

Now your code may look like this.

1
2
3
4
5
6
7
#undef IS_UNDEF
#define IS_UNDEF Z_EXPECTED_LONG // Which is zero
ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_LONG(foo)
ZEND_PARSE_PARAMETERS_END();
#undef IS_UNDEF
#define IS_UNDEF 0

Ugly, but your code will compile. Cheers :)

P.S. The latest PHP 7.2 still have this problem. Perhaps I should report this issue to the PHP internals guys.

NEVER use Chinese as system language on your Linux devices

During my attempt to execute a shell script provided by the manufacturer, I got a couple o’ annoying error outputs(shown below).

Going through the script several times, it seemed that there were nothing wrong with it. But it suddenly dawned on me that I was using Chinese as system language for my CentOS 7.

Then I found this. It’s obvious that grep Disk may not work as expected.

1
2
SIZE=`fdisk -l $DRIVE | grep Disk | awk '{print $5}'`
CYLINDERS=`echo $SIZE/255/63/512 | bc`

Gotcha..

The script worked after I changed system language to English. Perhaps we should never use Chinese(as well as other languages beside English) as system language. :)

P.S. In fact, you can do something like export LC_ALL="en_US.UTF-8" to switch locale insead of language, if you really wanna use other languages.

Plans For 20172

1. List of plans

  • Learn Java and Spring Boot.
  • Maintenance for TJUBBS backend. Including bug fix and adding minor functionalities.
  • Finish implementing all functionalities in php-asio and make a stable release.
  • Continue learning C# and Unity3D. Finish the VR project.
  • Continue learning data mining.

2. Optional

  • Write a client of YAP, and test the sanity of the demo server.
  • Rewrite RARBG crawler using amphp/aeyrs and amphp/artax for better performance.
  • Write an asynchronous MySQL client for Workerman.

Concurrency Programming in PHP

1. When is concurrency needed?

Bad examples

  • Attempting to fetch the content from a remote.
1
2
3
$handle = curl_init('http://www.an-extremely-slow-website.com/');
//This may take several seconds.
curl_exec($handle);
  • Performing a slow MySQL query.
1
2
3
4
$pdo = new PDO($dsn, $username, $password);
//There are million of rows in table `history`, query cannot use index.
$stmt = $pdo->prepare('SELECT * FROM `history` WHERE `file_name` NOT LIKE "%.jpg"');
$stmt->execute();
  • Network transmission is slow.
1
2
3
4
5
6
//File length is about 2MiBs in total.
while ($buffer = fread($handle, 8192)) {
//Unfortunately, Client can only recieve around 100KiBs per second.
fwrite($socket, $buffer, 8192);
}
fclose($handle);
  • Deferring is needed.
1
2
3
4
5
while ($buffer = fread($socket, 8192)) {
fwrite($handle, 8192);
//We want to restrict transmission speed by sleeping 100 milliseconds every 8KiBs.
usleep(100000);
}

What do they have in common?

  1. Something slow needs to be done.
  2. I/O process waits and does nothing.

Probable solutions

  1. Spawn a thread/process for each request.
  2. Handle requests asynchronously within one thread.

2. How to implement concurrency in PHP?

Event-based model

Event-driven libraries in PHP

  1. libevent
  2. libev
  3. libuv

Frameworks based on event-driven libraries

  1. ReactPHP
  2. Amp
  3. Workerman
  4. Swoole

Features and advantages

  1. Asynchronous, non-blocking I/O for web service, filesystem, and database connection, etc.
  2. Implementing high concurrency in a single thread. No cost for forking or spawning threads.

3. The Amp Framework

Basic usage

  • Event watchers
Watcher Type Callback Signature
defer() function (string $watcherId, $callbackData)
delay() function (string $watcherId, $callbackData)
repeat() function (string $watcherId, $callbackData)
onReadable() function (string $watcherId, $stream, $callbackData)
onWritable() function (string $watcherId, $stream, $callbackData)
onSignal() function (string $watcherId, $signal, $callbackData)
  • Controlling event watchers
Method Behaviour
run() Start the event loop with all watcher active.
stop() Terminate the event loop and continue execution to the next line after run().
enable() Resume a disabled watcher back to the event loop.
disable() Temporarily remove a watcher from the event loop.
reference() Mark a watcher as referenced.
unreference() Mark a watcher as unreferenced.
cancel() Destroy a watcher.
  • Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
use Amp\Loop;
//Same as call Loop::defer() with callback right before calling Loop::run() without callback.
Loop::run(function (string $id) {
echo "Event loop started. Watcher id is $id.\n";
//Get called right after this tick.
$id = Loop::defer(function (string $id, $param) {
echo "Watcher id is $id. Watcher id of last tick is $param.\n";
}, $id);
echo "Watcher id of next tick is $id.\n";
$count = 0;
//Loop::repeat() callbacks get called after every specified interval.
Loop::repeat(1000, function (string $id) use (&$count) {
++$count;
echo "Timer callback is called for $count time(s).\n";
if ($count == 7)
//Loop::delay() is similar with Loop::repeat(),
//except for that the former is destroyed right after its tick.
Loop::delay(2500, function () use ($id) {
//Loop::cancel() removes a specified watcher from the event loop.
Loop::cancel($id);
});
});
pcntl_signal(SIGINT, SIG_IGN);
//Get called whenever a specific signal is sent to the process.
$id = Loop::onSignal(SIGINT, function () {
echo "SIGINT received. Exiting event loop.\n";
//When Loop::stop() is called,
//the event loop will stop right after current tick.
Loop::stop();
});
//All watchers are referenced by default.
//Unreferenced watchers won't keep the event loop alive.
Loop::unreference($id);
});
//When there are no available watchers, the event loop exits automatically.
echo "Terminated.\n";

Promises

  • What are Promises?

  1. Asynchronous functions should return an instance of a class which implements Amp\Promise.
  2. Promises are created by an instance of Amp\Deferred, which resolves the promised value, and throws an exception when an error occurs.
  3. Unlike the Promises implemented in JavaScript and ReactPHP, etc, thenables in Amp are implemented with Coroutines.
  • Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
use Amp\Loop;
function asyncDivide($divisor, $dividend, $delay) {
//Promises are created by Amp\Deferred.
$deferred = new Amp\Deferred;
Loop::delay($delay, function () use ($divisor, $dividend, $deferred) {
$divisor = intval($divisor);
$dividend = intval($dividend);
if (!$dividend)
//Reject and emit an error.
$deferred->fail(new DivisionByZeroError('Divided by zero'));
else
//Resolve a result.
$deferred->resolve($divisor / $dividend);
});
//The async function shall return a Promise.
return $deferred->promise();
}
Loop::run(function () {
//Call a function asynchronously.
$promise = asyncDivide(4, 5, 1000);
//The following event occurs when the Promise is resolved or rejected.
$promise->onResolve(function (?Throwable $error, $result) {
if ($error)
echo $error->getMessage(), PHP_EOL;
else
echo 'Result is ',$result, PHP_EOL;
});
});
  • Promise Combinators (in namespace Amp\Promise) combine multiple promises to a single Promise.
Function Behaviour
all() Resolve when all Promises in the group resolve.
some() Resolve when no less than one Promise resolves.
any() Resolve even when all Promises fail.
first() Resolve when the first Promise in the group resolves.
  • Promise Helpers (in namespace Amp\Promise)
Function Behaviour
rethrow() Forward errors generated by the given Promise to the event loop.
timeout() Throw an exception if the given Promise fail to resolve or reject.
wait() Synchronously wait for a Promise to resolve.

Coroutines

  • What are coroutines?

  • In Amp, all yields of coroutines must be one of the following type.
Type Description
Amp\Promise Control will be returned to the Coroutine once resolved.
React\Promise\PromiseInterface Will be adapted to Amp\Promise.
array Array of Promises will be combined implicitly to Amp\Promise\All.
  • Coroutine helpers (in Amp namespace)
Function Behaviour
coroutine(callable $callback) : callable Wrap a function into a coroutine.
asyncCoroutine(callable $callback) : callable Callback function do not return a Promise when called.
call(callable $callback, …$args) : Promise Call the given function, and return a Promise.
asyncCall(callable $callback, …$args) : void Do not return a Promise.
  • Examples:
1
2
3
4
5
6
7
8
9
10
function asyncDivide($divisor, $dividend, $delay) {
return \Amp\coroutine(function () use ($divisor, $dividend, $delay) {
yield new Amp\Delayed($delay);
return $divisor / $dividend;
});
}
Amp\Loop::run(function () {
$value = yield asyncDivide(3, 4, 500)();
var_dump($value);
});
1
2
3
4
5
6
7
8
9
10
function asyncDivide($divisor, $dividend, $delay) {
return Amp\call(function () use ($divisor, $dividend, $delay) {
yield new Amp\Delayed($delay);
return $divisor / $dividend;
});
}
Amp\Loop::run(function () {
$value = yield asyncDivide(3, 4, 500);
var_dump($value);
});

Iterators

  • In Amp, an iterator iterates through a set of Promises, and resolves alongside with the Promises. It can be recognized as a “special” Promise which can be resolved multiple times.
  • Iterators are created by Amp\Emitter.
  • Iterator functions are listed below.
Method Behaviour
Iterator::getCurrent() If Promise resolves to true, consume value of current position.
Iterator::advance() Return a Promise which indicates whether there’s a value to consume.
Emitter::emit() Emits a new value to the Iterator.
Emitter::complete() Mark an iterator as complete and no further emits will be done.
Emitter::iterate() Return instance of Iterator.
Iterator\fromIterable() Converts arrays or Traversable objects into an Iterator.
  • Examples:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function subtractToZero($init, $interval) {
$value = $init;
//Iterators are created by Amp\Emitter.
$emitter = new Amp\Emitter;
Loop::repeat($interval, function ($id) use ($emitter, &$value) {
if ($value > 0)
$emitter->emit(--$value);
else {
$emitter->complete();
//Cancel timer event when complete.
Loop::cancel($id);
}
});
//Return the iterator.
return $emitter->iterate();
}
Loop::run(function () {
$iterator = subtractToZero(10, 100);
while (yield $iterator->advance())
var_dump($iterator->getCurrent());
});
  • Producer is a simplified form of emitter which can be used when all values can be emitted in a single coroutine.
1
2
3
4
5
6
7
8
9
10
11
Amp\Loop::run(function () {
$iterator = new \Amp\Producer(function (callable $emit) {
static $i = 0;
while (++$i < 10) {
yield new \Amp\Delayed(200);
yield $emit($i);
}
});
while (yield $iterator->advance())
var_dump($iterator->getCurrent());
});
  • Iterator combination functions combine an array of Iterators into a single Iterator.
Function Behaviour
Iterator\concat() Iterators are resolved one by one.
Iterator\merge() Iterators are resolved simultaneously.
  • Iterator transformation functions intervene the resolution of Iterators using Producer.
Function Behaviour
Iterator\map() Transform the resolved value into another value.
Iterator\filter() Resolved value is omitted if filter callback returns false.
1
2
3
4
5
6
7
Loop::run(function () {
$iterator = \Amp\Iterator\map(subtractToZero(10, 200), function ($value) {
return "Current value is $value.\n";
});
while (yield $iterator->advance())
echo $iterator->getCurrent();
});
1
2
3
4
5
6
7
Loop::run(function () {
$iterator = \Amp\Iterator\filter(subtractToZero(10, 200), function ($value) {
return $value != 3;
});
while (yield $iterator->advance())
var_dump($iterator->getCurrent());
});

Cancellation

  • Amp provides cancellation of a specific asynchronous operation. but it does not and cannot automatically handle cancellation. Instead, you should handle cancellation manually after its request.
  • Cancellation is implemented using Amp\CancellationTokenSource and Amp\CancellationToken.
Method Behaviour
CancellationTokenSource::getToken() Returns a unique CancellationToken instance.
CancellationTokenSource::cancel() Emits a Cancellation request to its CancellationToken.
CancellationToken::isRequested() Resurns whether there is a Cancellation request.
CancellationToken::throwIfRequested() Throws CancelledException if Cancellation request exists.
CancellationToken::subscribe() Callback will be invoked when the request occurs.
CancellationToken::unsubscribe() Disable a specified callback by id.
  • Examples:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use Amp\Loop;
function subtractToZero($init, $interval, $token = null) {
$value = $init;
$emitter = new Amp\Emitter;
Loop::repeat($interval, function ($id) use ($emitter, &$value, $token) {
//Cancellation requests are received by isRequested() method.
if ($value > 0 && (!isset($token) || !$token->isRequested()))
$emitter->emit(--$value);
else {
$emitter->complete();
Loop::cancel($id);
}
});
return $emitter->iterate();
}
Loop::run(function () {
$token_source = new \Amp\CancellationTokenSource;
$iterator = subtractToZero(10, 200, $token_source->getToken());
Loop::delay(1500, function () use ($token_source) {
//Cancel this operation 1500 milliseconds after current tick.
$token_source->cancel();
});
while (yield $iterator->advance())
var_dump($iterator->getCurrent());
});
1
2
3
4
5
6
7
8
9
10
11
12
13
Loop::repeat($interval, function ($id) use ($emitter, &$value, $token) {
//Callback which is subscribed to a Cancellation Token
//will be invoked before the callback marked as cancelled.
$token->subscribe(function () use ($id, $emitter) {
Loop::cancel($id);
});
if ($value > 0)
$emitter->emit(--$value);
else {
$emitter->complete();
Loop::cancel($id);
}
});