Porting Extensions to PHP 8

open phpPHP 8 has been the biggest change to PHP in years. While PHP 8’s JIT compiler gets most of the publicity, more significant to most developers would be PHP 8’s changes that encourage better coding practices. PHP 8 pushes developers to use clearer syntax and is stricter with problematic code.

While the PHP runtime itself has improved, what about extensions such as ibm_db2? What changes do extension developers need to make to adapt to PHP 8? As maintainers of the ibm_db2 and PDO_IBM database extensions, we’ve learned what it takes to make PHP extensions compatible with PHP 8.

Argument Info Is Mandatory

In PHP 8, if the extension doesn’t provide argument information (“arginfo”) for a function, it’ll run, but you’ll get messages like:

Warning: Missing arginfo for db2_connect() in Unknown on line 0
Warning: Missing arginfo for db2_commit() in Unknown on line 0

PHP 8 is more strict about requiring the extension to provide at least the argument name, whether the argument is a reference, and whether it’s required. Having the arginfo helps the PHP runtime catch issues before it reaches the extension’s functions.

Since the ibm_db2 extension didn’t have any arginfo before, we had to add some:

// the first argument is the name of this arginfo block
// the second argument is always zero (reserved for future use)
// the third argument is if it returns a reference
// the fourth argument is how many arguments are required
ZEND_BEGIN_ARG_INFO_EX(arginfo_db2_connect, 0, 0, 3)
 // the first argument to this is if this is a reference
 // the second argument to this is the name
 ZEND_ARG_INFO(0, database)
 ZEND_ARG_INFO(0, username)
 ZEND_ARG_INFO(0, password)
 ZEND_ARG_INFO(0, options)
ZEND_END_ARG_INFO()

// and so on

There are options for adding even more data in argument info like default values, but for now, we’ll just state the minimum required. Once we have the argument info defined for each function, we’ll need to change each line in the list of functions to point to the argument info:

 zend_function_entry ibm_db2_functions[] = {
- PHP_FE(db2_connect, NULL)
- PHP_FE(db2_commit, NULL)
- PHP_FE(db2_pconnect, NULL)
- PHP_FE(db2_autocommit, NULL)
- PHP_FE(db2_bind_param, NULL)
- PHP_FE(db2_close, NULL)
+ PHP_FE(db2_connect, arginfo_db2_connect)
+ PHP_FE(db2_commit, arginfo_db2_commit)
+ PHP_FE(db2_pconnect, arginfo_db2_pconnect)
+ PHP_FE(db2_autocommit, arginfo_db2_autocommit)
+ PHP_FE(db2_bind_param, arginfo_db2_bind_param)
+ PHP_FE(db2_close, arginfo_db2_close)

// and so on

Once we do that, PHP is satisfied and won’t complain anymore.

In the case of PDO_IBM, this missing arginfo helped catch an entirely unnecessary function that was left in place from the template that PHP provides for extensions. Since we don’t need it, we could get rid of it entirely.

RIP, Thread-Safe Resource Manager Macros

In the ancient days of PHP 5, the Thread-Safe Resource Manager made its mark on extensions. Many functions had to carry around macros to make room for the TSRM’s extra arguments. In PHP 7, thread safety went under a major overhaul, and the old TSRM macros were made inert. In PHP 8, they were removed completely, but one can simply reinstate these macros and avoid having to remove the remnants of a bygone age.

#ifndef TSRMLS_D
#define TSRMLS_D void
#define TSRMLS_DC
#define TSRMLS_C
#define TSRMLS_CC
#define TSRMLS_FETCH()
#endif

To get rid of these macros completely, a simple regular expression can remove them. This one handles a leading space (like the ones that are part of an argument list) and a few of the variants of the macro that were in use.

s/ ?TSRMLS_[DC][DC]?//g

Cleaning Up

There’s been a lot of PHP 5-specific code lurking in the extensions. While this code doesn’t impact PHP 8 compatibility (since it’s all conditional on version), it does make understanding and maintaining the code harder. Since PHP 5 support has been dropped, why not drop hundreds of lines of #ifdef?

Doing so manually is tedious, but the unifdef program can make your life a lot easier. It takes the definitions to evaluate, and it will only remove those checks, even if they’re nested inside something else. While it doesn’t excise all of the PHP 5 compatibility code, it gets most of it. For example, to remove the PHP 5-specific code, we’d just specify the macro so that we’re at PHP 7:

$ unifdef -DPHP_MAJOR_VERSION=7 -m ibm_db2.c

Semicolons

PHP’s headers include a lot of macros to streamline common operations when writing an extension, like returning a string. Before PHP 8, the macros included an ending semicolon for you. However, not including your own semicolon looked awkward. PHP 8 solves this, so we’ll have to fix code that depends on the old behaviour:

- ZEND_RETURN_STRINGL(new_str, new_length, 1) 
+ ZEND_RETURN_STRINGL(new_str, new_length, 1);

Return Types

PHP 8 is migrating many extension function return types to a zend_return, instead of semantically vague generic types like int. This is minor, but it can be an API break you’ll need to deal with regardless.

Stricter, Even in Tests

PHP 8 is much stricter, like elevating warnings to fatal errors, turning messages into catchable exceptions and errors, and making fatal errors impossible to silence. Sometimes the runtime might even throw more severe errors than your extension handles. Some tests intentionally look at dodgy behaviour (like passing null in an obviously bogus place), so you can remove these tests or adjust the expected output to match. If a test uses a regex or format strings to check expected output, it’s easy to change those.

When tests were checking bad behaviour, older PHP would usually try it and let the extension deal with the fallout. Now, the runtime or even middle layers like PDO can catch these issues and throw different kinds of exceptions. In one case, this meant that the PDO_IBM extension was no longer setting the SQL-side error, because PDO had caught the error before it could even reach the extension! We simply caught the exception that PDO threw, and the test works on both PHP 7 and PHP 8.

PHP’s new strictness will help you catch issues in tests that were previously passing. ibm_db2’s tests had some obviously broken variable accesses that were now exposed!

- $stmt = @db2_exec(conn,drop_proc_sql);
+ $stmt = @db2_exec($conn, $drop_proc_sql);

PHP 8 Isn’t a Big Migration for Extensions

The changes that we needed to make to get extensions up and running were minor. Even though the argument info changes were quite long, they were simple and done mechanically. While PHP 7 was a big change for extensions, the changes for a well-behaved extension in PHP 8 are small. Usually, it’s just cleaning up old code that could have used a refactor anyway. Unfortunately, a lot of the resources for PHP extension authors are outdated or simply not written yet. Hopefully this article helps you when migrating any extensions you maintain or require to PHP 8.

Edit 2021/03/24: Mention return type changes.

2 replies
    • Calvin Buckley
      Calvin Buckley says:

      I didn’t notice that, because the extensions I was porting were old-school very imperative extensions, and PDO was providing the object stuff for the other one. Any details how object handlers have changed?

      Reply

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.