This chapter introduces the Foreign Function Interface (FFI). In this chapter, you will learn what FFI is all about, what it's good for, and how to use it. This information in this chapter is important for developers interested in rapid custom prototyping using direct C-language calls.
In this chapter, not only do you learn about the background behind introducing FFI into the PHP language, but you also learn how to incorporate C-language structures and functions directly into your code. Although—as you will learn—this should not be done to achieve greater speed, it does give you the ability to incorporate any C-language libraries directly into your PHP application. This ability opens the doors to an entire world of functionality hitherto unavailable to PHP.
Topics covered in this chapter include the following:
To examine and run the code examples provided in this chapter, the minimum recommended hardware is listed here:
In addition, you will need to install the following software:
Please refer to the Technical requirements section of Chapter 1, Introducing New PHP 8 OOP Features, for more information on Docker and Docker Compose installation, as well as how to build the Docker container used to demonstrate code explained in this book. In this book, we refer to the directory in which you restored the sample code as /repo.
The source code for this chapter is located here:
https://github.com/PacktPublishing/PHP-8-Programming-Tips-Tricks-and-Best-Practices
We can now begin our discussion by gaining an understanding of FFI.
The main purpose of a FFI is to allow any given programming language the ability to incorporate code and function calls from external libraries written in other languages. An early example of this was the ability of 1980s microcomputers to incorporate assembler language into otherwise sluggish Beginners' All-purpose Symbolic Instruction Code (BASIC) programming language scripts using the PEEK and POKE commands. Unlike many other languages, PHP did not have this capability prior to PHP 7.4, although it had been under discussion since 2004.
In order to gain a full understanding of FFI in PHP 8, it's necessary to digress and have a look at why it took so long for FFI to be fully adopted into the PHP language. It's also necessary to take a quick look at PHP extensions in general, and the ability to work with C-language code. We first examine the relationship between PHP and the C language.
The C language was developed at Bell Labs by Dennis Ritchie in late 1972. Since that time, despite the introduction of its object-oriented cousin C++, this language continues to dominate the programming language landscape. PHP itself is written in C; accordingly, the ability to directly load C-shared libraries, and to gain direct access to C functions and data structures, is an incredibly important addition to the PHP language.
The introduction of the FFI extension into the PHP language gives PHP the ability to load and directly work with both C structures and C functions. In order to make intelligent decisions about where and when you might want to use the FFI extension, let's look at PHP extensions in general.
PHP extensions, as the title implies, extend the PHP language. Each extension can add object-oriented programming (OOP) classes as well as procedural-level functions. Each extension serves a distinct logical purpose—for example, the GD extension handles graphic image manipulation, while the PDO extension handles database access.
As an analogy, consider a hospital. In the hospital, you have departments such as Emergency, Surgery, Pediatrics, Orthopedics, Cardiac, X-Ray, and so forth. Each department is self-contained and serves a distinct purpose. Collectively the departments form the hospital. In a like manner, PHP is like the hospital, and its extensions are like the various departments.
Not all extensions are equal. Some extensions, referred to as core extensions, are always available when PHP is installed. Other extensions must be downloaded, compiled, and enabled manually. Let's now have a look at core extensions.
PHP core extensions are directly included in the main PHP source code repository located here: https://github.com/php/php-src/tree/master/ext. If you go to this web page, you'll see a list of subdirectories, as shown in the following screenshot. Each subdirectory contains C-language code comprising the particular extension:
Thus, when PHP is installed on a server, all of the core extensions are compiled and installed as well. We will now have a brief look at extensions that are not part of the core.
PHP extensions that are not part of the core are usually maintained by a specific vendor (Microsoft is an example). Very typically, non-core extensions are considered optional and are not widely used.
Once a non-core extension starts getting used more and more frequently, it's quite possible that it will eventually be migrated into the core. Examples of this are numerous. The most recent is the JSON extension: it's now not only part of the core, but in PHP 8 this extension can no longer be disabled.
It's also possible for a core extension to be removed. An example of this is the mcrypt extension. This was deprecated in PHP 7.1 as the underlying library upon which this extension relied had been abandoned for over 9 years. In PHP 7.2, it was formally removed from the core. We now consider where to find non-core extensions.
A logical question you might ask at this point is: Where do you get non-core extensions? In general, non-core extensions are available directly from the vendor, from github.com, or from this website: http://pecl.php.net/. There have been complaints over the years that pecl.php.net contains outdated and unmaintained code. Although this is partially true, it is also true that up-to-date, actively maintained code does exist on this website.
As an example, if you have a look at the PHP extension for MongoDB, you'll see that the last release was at the end of November 2020. The following screenshot shows the PHP Extension Community Library (PECL) website page for this extension:
In many cases, the vendor prefers to retain full control over the extension. This means you need to go to their website to obtain the PHP extension. An example of this is the PHP extension for Microsoft SQL Server, found at this Uniform Resource Locator (URL): https://docs.microsoft.com/en-us/sql/connect/php/download-drivers-php-sql-server?view=sql-server-ver15.
The key takeaway from this subsection is that the PHP language is enhanced through its extensions. The extensions are written in the C language. Accordingly, the ability to model the logic of a prototype extension directly inside a PHP script is extremely important. Let's now turn our attention to where you should use FFI.
The potential for importing C libraries directly into PHP is truly staggering. One of the PHP core developers actually used the FFI extension to bind PHP to the C-language TensorFlow machine learning platform!
Tip
For information on the TensorFlow machine learning platform, head over to this web page: https://www.tensorflow.org/. To see how PHP can be bound to this library, have a look here: https://github.com/dstogov/php-tensorflow.
As we show you in this section, the FFI extension is not a magic solution for all of your needs. This section discusses the main strengths and weaknesses of the FFI extension, as well as giving you guidelines for its use. A myth we debunk in this section is that making direct C-language calls using the FFI extension speeds up PHP 8 program execution. First, let's have a look at what took so long to get the FFI extension into PHP.
The first FFI extension was actually introduced for PHP 5 on the PECL website (https://pecl.php.net/) in January 2004 by PHP core developers Wez Furlong and Ilia Alshanetsky. The project never passed its Alpha stage, however, and development was dropped within a month.
As PHP developed and matured over the next 14 years, it became apparent that PHP would benefit from the ability to rapidly prototype potential extensions directly within a PHP script. Without this capability, PHP was in danger of falling behind other languages such as Python and Ruby.
In the past, lacking a fast-prototyping capability, extension developers were forced to compile their full extension and install it using pecl, before being able to test it in a PHP script. In some cases, developers had to recompile PHP itself just to test their new extension! In contrast, the FFI extension allows a developer to directly place C function calls inside the PHP script for immediate testing.
Starting with PHP 7.4 and carried on into PHP 8, an improved version of the FFI extension was proposed by core developer Dmitry Stogov. After a compelling proof of concept (see the preceding Tip box regarding PHP binding to the TensorFlow machine learning platform), this FFI extension version was incorporated into the PHP language.
Tip
The original FFI PHP extension can be found here: http://pecl.php.net/package/ffi. For more information on the revised FFI proposal, see the following article: https://wiki.php.net/rfc/ffi.
Let's now examine why FFI should not be used to gain speed.
Because the FFI extension allows PHP direct access to C-language libraries, there is a temptation to believe that your PHP applications will suddenly operate blindingly fast, at machine-language speeds. Unfortunately, this is not the case. The FFI extension needs to first open the given C library and then parse and pseudo-compile a FFI instance before execution. The FFI extension then acts as a bridge between the C-library code and the PHP script.
It might be of relief to some readers that relatively sluggish FFI extension performance is not limited to PHP 8. Other languages suffer the same throttling effect when using their own FFI implementations. There's an excellent performance comparison, based upon the Ary 3 benchmark, available here: https://wiki.php.net/rfc/ffi#php_ffi_performance.
If you have a look at the table shown on the web page just referenced, you'll see that the Python FFI implementation performed the benchmark in 0.343 seconds, whereas running the same benchmark using only native Python code executed in 0.212 seconds.
Looking at the same table, the PHP 7.4 FFI extension ran the benchmark in 0.093 seconds (30 times faster than Python!), whereas the same benchmark running with just native PHP code executed in 0.040 seconds.
The next logical question is: Why should you use the FFI extension at all? This is covered in the next section.
The answer to the preceding question is simple: this extension is primarily designed for rapid PHP extension prototyping. PHP extensions are the lifeblood of the language. Without extensions, PHP is just another programming language.
When senior-level developers first embark upon a programming project, they need to determine the best language for the project. One key factor is how many extensions are available and how actively these are maintained. There is generally a direct correlation between the number of actively maintained extensions and the long-term success potential of a project using that language.
So, if there's a way to speed up extension development, the long-term viability of the PHP language itself is improved. The value the FFI extension brings to the PHP language is its ability to test an extension prototype directly in a PHP script without having to go through the entire compile-link-load-test cycle.
Another use case for the FFI extension, outside of rapid prototyping, is a way to allow PHP direct access to obscure or proprietary C code. An example of this would be the custom C code written to control factory machines. In order to have PHP run the factory, the FFI extension can be used to bind PHP directly to the C libraries controlling the various machines.
Finally, another use case for this extension is to use it to preload C libraries, potentially reducing memory consumption. Before we show usage examples, let's have a look at the FFI class and its methods.
As you learned in this chapter, not every developer has a need to use the FFI extension. Having direct experience with the FFI extension deepens your understanding of the internals of the PHP language, and this deepened understanding can have a beneficial impact on your career as a PHP developer: it's quite possible that at some point in the future, you will be employed by a company that has developed a custom PHP extension. Knowing how to operate the FFI extension in this situation allows you to develop new features for a custom PHP extension, as well as helping you to troubleshoot extension problems.
The FFI class consists of 20 methods that fall into four broad categories, outlined as follows:
Tip
The complete FFI class is documented here: https://www.php.net/manual/en/class.ffi.php.
Interestingly, all FFI class methods can be called in a static manner. It's now time to take a dive into the details and usage of the class associated with FFI, starting with the creational methods.
The FFI methods that fall into the creational category are designed to produce either FFI instances directly or instances of classes provided by the FFI extension. When working with C functions made available through the FFI extension, it's important to recognize that you cannot directly pass native PHP variables into the function and expect it to work. The data must first be either created as a FFI data type or imported into a FFI data type, before the FFI data type can be passed into the C function. In order to create a FFI data type, use one of the functions summarized in Table 4.1, shown here:
Both the cdef() and scope() methods produce a direct FFI instance, while the other methods produce object instances that can be used to create a FFI instance. string() is used to extract a given number of byes from a native C variable. Let's have a look at creating and using FFICType instances.
It's extremely important to note that once the FFICType instance has been created, do not simply assign a value to it as if it were a native PHP variable. Doing so would simply overwrite the FFICType instance due to the fact that PHP is loosely typed. Instead, to assign a scalar value to a FFICType instance, use its cdata property.
The following example creates a $arr C array. The native C array is then populated with values up to its maximum size, after which we use a simple var_dump() to view its contents. We will proceed as follows:
// /repo/ch04/php8_ffi_array.php
$type = FFI::arrayType(FFI::type("char"), [3, 3]);
$arr = FFI::new($type);
$arr = FFI::new(FFI::type("char[3][3]"));
$pos = 0;
$val = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
$y_max = count($arr);
for ($y = 0; $y < $y_max; $y++) {
$x_max = count($arr[$y]);
for ($x = 0; $x < $x_max; $x++) {
$arr[$y][$x]->cdata = $val[$pos++];
}
}
var_dump($arr)
In the preceding example, we use nested for() loops to populate the two-dimensional 3 x 3 array with letters of the alphabet. If we now execute a simple var_dump(), we get the following result:
root@php8_tips_php8 [ /repo/ch04 ]# php
php8_ffi_array.php
object(FFICData:char[3][3])#2 (3) {
[0]=> object(FFICData:char[3])#3 (3) {
[0]=> string(1) "A"
[1]=> string(1) "B"
[2]=> string(1) "C"
}
[1]=> object(FFICData:char[3])#1 (3) {
[0]=> string(1) "D"
[1]=> string(1) "E"
[2]=> string(1) "F"
}
[2]=> object(FFICData:char[3])#4 (3) {
[0]=> string(1) "G"
[1]=> string(1) "H"
[2]=> string(1) "I"
}
The first important thing to note from the output is that the indices are all integers. The second takeaway from the output is that this is clearly not a native PHP array. var_dump() shows us that each array element is a FFICData instance. Also, note that C-language strings are treated like an array.
Because the array is of type char, we can use FFI::string() to display one of the rows. Here is a command that produces an ABC response:
echo FFI::string($arr[0], 3);
Any attempt to supply the FFICData instance to a PHP function that takes an array as an argument is doomed to failure, even if it is defined as an array type. In the following code snippet, note the output if we add this command to the preceding code block:
echo implode(',', $arr);
As you can see from the output shown next, because the data type is not array, implode() issues a fatal error. Here is the resulting output:
PHP Fatal error: Uncaught TypeError: implode(): Argument #2 ($array) must be of type ?array, FFICData given in /repo/ch04/php8_ffi_array.php:25
You know now how to create and use FFICType instances. Let's now turn our attention to creating FFI instances.
As mentioned in the chapter introduction, the FFI extension facilitates rapid prototyping. Accordingly, using the FFI extension, you can develop the C functions designed to go into your new extension one at a time, and test them right away inside a PHP application.
Important note
The FFI extension does not compile C code. In order to use a C function with the FFI extension, you must first compile the C code into a shared library using a C compiler. You will learn how to do this in the last section in this chapter, Using FFI in an application.
In order to bridge between PHP and a native C-library function call, you need to create a FFI instance. The FFI extension needs you to supply a C definition that defines the C function signature and the C library that you plan to use. Both FFI::cdef() and FFI::scope() can be used to directly create FFI instances.
The following example uses FFI::cdef() to bind two native C-library functions. This is what happens:
// /repo/ch04/php8_ffi_cdef.php
$key = '';
$size = 4;
$code = <<<EOT
void srand (unsigned int seed);
int rand (void);
EOT;
$ffi = FFI::cdef($code, 'libc.so.6');
$ffi->srand(random_int(0, 999));
for ($x = 0; $x < $size; $x++)
$key .= sprintf('%x', $ffi->rand());
echo $key
And here is the output of the preceding code snippet. Note that the resulting value could be used as a random key:
root@php8_tips_php8 [ /repo/ch04 ]# php php8_ffi_cdef.php
23f306d51227432e7d8d921763b7eedf
In the output, you see a string of concatenated random integers converted to hexadecimal. Note that the resulting value changes each time the script is invoked.
Tip
For true randomization, it might be better to just use the random_int() native PHP function. There are also excellent key-generation functions that form part of the openssl extension. The example shown here is primarily designed to familiarize you with FFI extension usage.
Important note
The FFI extension also includes two additional creational methods: FFI::load() and FFI::scope(). FFI::load() is used to directly load C-function definitions from a C header (*.h) file during the preloading process. FFI::scope() makes the preloaded C functions available for use via the FFI extension. For more information on preloading, have a look at a complete preloading example in the FFI documentation here: https://www.php.net/manual/en/ffi.examples-complete.php.
Let's now have a look at FFI extension functions used for comparison between native C data types.
It's important to keep in mind that when you create a C-language data structure using the FFI extension, it exists outside of your PHP application. As you saw in the preceding example (see the Creating and using FFICType instances section), PHP can interact with the C data to a certain extent. However, for comparison purposes, it's best to use FFI::memcmp(), as native PHP functions might return inconsistent results.
The two comparison functions available in the FFI extension are summarized here in Table 4.2:
FFI::isNull() can be used to determine whether or not the FFICData instance is NULL. What is more interest is FFI::memcmp(). Although this function operates in the same manner as the spaceship operator (<=>), it accepts a third argument that represents how many bytes you wish to include in the comparison. The following example illustrates this usage:
// /repo/ch04/php8_ffi_memcmp.php
$a = FFI::new("char[6]");
$b = FFI::new("char[6]");
$c = FFI::new("char[6]");
$d = FFI::new("char[6]");
$populate = function ($cdata, $start, $offset, $num) {
for ($x = 0; $x < $num; $x++)
$cdata[$x + $offset] = chr($x + $offset +
$start);
return $cdata;
};
$a = $populate($a, 65, 0, 6);
$b = $populate($b, 65, 0, 3);
$b = $populate($b, 85, 3, 3);
$c = $populate($c, 71, 0, 6);
$d = $populate($d, 71, 0, 6);
$patt = "%2s : %6s ";
printf($patt, '$a', FFI::string($a, 6));
printf($patt, '$b', FFI::string($b, 6));
printf($patt, '$c', FFI::string($c, 6));
printf($patt, '$d', FFI::string($d, 6));
$a : ABCDEF
$b : ABCXYZ
$c : GHIJKL
$d : GHIJKL
PHP Fatal error: Uncaught FFIException: Comparison of incompatible C types
PHP Warning: strcmp() expects parameter 1 to be string, object given
$p = "%20s : %2d ";
printf($p, 'memcmp($a, $b, 6)', FFI::memcmp($a,
$b, 6));
printf($p, 'memcmp($c, $a, 6)', FFI::memcmp($c,
$a, 6));
printf($p, 'memcmp($c, $d, 6)', FFI::memcmp($c,
$d, 6));
memcmp($a, $b, 6) : -1
memcmp($c, $a, 6) : 1
memcmp($c, $d, 6) : 0
echo " Using FFI::memcmp() but not full length ";
printf($p, 'memcmp($a, $b, 3)', FFI::memcmp($a,
$b, 3));
Using FFI::memcmp() but not full length
memcmp($a, $b, 3) : 0
The most important thing to take away from this illustration is that you need to find a balance between the number of characters to compare and the nature of the data you are comparing. The fewer characters compared, the faster the overall operation. However, if the nature of the data is such that erroneous results are possible, you must increase the character count and suffer a slight loss in performance.
Let's now have a look at gathering information from FFI extension data.
When you are using FFI instances and native C data structures, native PHP informational methods such as strlen() and ctype_digit() do not yield useful information. Accordingly, the FFI extension includes three methods designed to produce information about FFI extension data. These three methods are summarized here in Table 4.3:
We first look at FFI::typeof(), after which we dive into the other two methods.
Here is an example that illustrates the use of FFI::typeof(). The example also demonstrates that native PHP informational functions do not yield useful results when dealing with FFI data. This is what we do:
// /repo/ch04/php8_ffi_typeof.php
$char = FFI::new("char[6]");
for ($x = 0; $x < 6; $x++)
$char[$x] = chr(65 + $x);
try {
echo 'Length of $char is ' . strlen($char);
} catch (Throwable $t) {
echo $t::class . ':' . $t->getMessage();
}
TypeError:strlen(): Argument #1 ($str) must be of type string, FFICData given
echo '$char is ' .
((ctype_alnum($char)) ? 'alpha' : 'non-alpha');
$char is non-alpha
$type = FFI::typeOf($char);
var_dump($type);
object(FFICType:char[6])#3 (0) {}
As you can see from the final output, we now have useful information! Let's now have a look at the other two FFI informational methods.
Before getting into a practical example showing the use of these two methods, it's important to understand what exactly is meant by alignment. In order to understand alignment, you need to have a basic understanding of how memory is organized in most computers.
RAM is still the fastest way to temporarily store information used during a program run cycle. Your computer's central processing unit (CPU) moves information in and out of memory as the program executes. Memory is organized in parallel arrays. The alignment value returned by alignof() would be how many bytes can be obtained at once from parallel slices of the aligned memory arrays. In older computers, a value of 4 was typical. For most modern microcomputers, values of 8 or 16 (or greater) are common.
Let's now have a look at an example that illustrates how these two FFI extension informational methods are used, and how that information can produce a performance improvement. This is how we'll proceed:
$struct = 'struct Bad { char c; double d; int i; }; '
. 'struct Good { double d; int i; char c; };
';
$ffi = FFI::cdef($struct);
$bad = $ffi->new("struct Bad");
$good = $ffi->new("struct Good");
var_dump($bad, $good);
object(FFICData:struct Bad)#2 (3) {
["c"]=> string(1) ""
["d"]=> float(0)
["i"]=> int(0)
}
object(FFICData:struct Good)#3 (3) {
["d"]=> float(0)
["i"]=> int(0)
["c"]=> string(1) ""
}
echo " Bad Alignment: " . FFI::alignof($bad);
echo " Bad Size: " . FFI::sizeof($bad);
echo " Good Alignment: " . FFI::alignof($good);
echo " Good Size: " . FFI::sizeof($good);
The last four lines of output from this code example are shown here:
Bad Alignment: 8
Bad Size: 24
Good Alignment: 8
Good Size: 16
As you can see from the output, the return from FFI::alignof() tells us that the alignment blocks are 8 bytes wide. However, you can also see that the size in bytes taken up by the Bad structure is 50% larger than the size required for the Good structure. Since the two data structures have exactly the same properties, any developer in their right mind would choose the Good structure.
From this example, you can see that the FFI extension informational methods are able to give us an idea on how best to structure our C data in order to produce the most efficient results.
Tip
For an excellent discussion on the difference between sizeof() and alignof() in the C language, see this article: https://stackoverflow.com/questions/11386946/whats-the-difference-between-sizeof-and-alignof.
You now have an understanding of what the FFI extension informational methods are and have seen some examples of their use. Let's now have a look at the FFI extension methods pertaining to infrastructure.
FFI extension infrastructural category methods can be thought of as behind-the-scenes components that support the infrastructure needed for C function binding to work properly. As we have stressed throughout this chapter, the FFI extension is needed if you wish to directly access C data structures from within a PHP application. Thus, if you need to do the equivalent of a PHP unset() statement to release memory, or a PHP include() statement to include external program code, the FFI extension infrastructural methods provide the bridge between native C data and PHP.
Table 4.4, shown here, summarizes methods in this category:
Let's first have a look at FFI::addr(), free(), memset(), and memcpy().
PHP developers often assign a value to a variable by reference. This allows a change in one variable to be automatically reflected in another. The use of references is especially useful when passing parameters to a function or method where you need to return more than a single value. Passing by reference allows the function or method to return an unlimited number of values.
The FFI::addr() method creates a C pointer to an existing FFICData instance. Just as with a PHP reference, any changes made to the data associated with the pointer will likewise be changed.
In the process of building an example using the FFI::addr() method, we also introduce you to FFI::memset(). This function is much like the str_repeat()PHP function, in that it (FFI::memset()) populates a specified number of bytes with a specific value. In this example, we use FFI::memset() to populate a C character string with letters of the alphabet.
In this subsection, we also have a look at FFI::memcpy(). This function is used to copy data from one FFICData instance to another. Unlike the FFI::addr() method, FFI::memcpy() creates a clone that has no connection to the source of the copied data. In addition, we introduce FFI::free(), a method used to release a pointer created using FFI::addr().
Let's have a look at how these FFI extension methods can be used, as follows:
// /repo/ch04/php8_ffi_addr_free_memset_memcpy.php
$size = 6;
$arr = FFI::new(FFI::type("char[$size]"));
FFI::memset($arr, 65, $size);
echo FFI::string($arr, $size);
AAAAAA
$arr2 = FFI::new(FFI::type("char[$size]"));
FFI::memcpy($arr2, $arr, $size);
echo FFI::string($arr2, $size);
AAAAAA
$ref = FFI::addr($arr);
FFI::memset($ref[0], 66, 6);
echo FFI::string($arr, $size);
var_dump($ref, $arr, $arr2);
BBBBBB
object(FFICData:char(*)[6])#2 (1) {
[0]=> object(FFICData:char[6])#4 (6) {
[0]=> string(1) "B"
[1]=> string(1) "B"
[2]=> string(1) "B"
[3]=> string(1) "B"
[4]=> string(1) "B"
[5]=> string(1) "B"
}
}
object(FFICData:char[6])#3 (6) {
[0]=> string(1) "B"
[1]=> string(1) "B"
[2]=> string(1) "B"
[3]=> string(1) "B"
[4]=> string(1) "B"
[5]=> string(1) "B"
}
object(FFICData:char[6])#4 (6) {
[0]=> string(1) "A"
[1]=> string(1) "A"
[2]=> string(1) "A"
[3]=> string(1) "A"
[4]=> string(1) "A"
[5]=> string(1) "A"
}
As you can see from the output, we first have a BBBBBB string. You can see that the pointer is in the form of a PHP array. The original FFICData instance, $arr, has now changed to letter B. However, the preceding output also clearly shows that the copy, $arr2, is not affected by changes made to $arr or its $ref[0] pointer.
FFI::free($ref);
Now that you have an idea about how to work with C pointers and about populating C data with information, let's have a look at how to do type casting with a FFICData instance.
In PHP, the process of type casting occurs quite frequently. It's used when PHP is asked to perform operations involving dissimilar data types. A classic example of this is shown in the following block of code:
$a = 123;
$b = "456";
echo $a + $b;
In this trivial example, $a is assigned a data type of int (integer) and $b is assigned a type of string. The echo statement requires PHP to first typecast $b to int, perform the addition, and then typecast the result to string.
Native PHP also allows the developer to force the data type by prepending the desired data type in parentheses in front of the variable or expression. The rewritten example from the previous code snippet might appear as follows:
$a = 123;
$b = "456";
echo (string) ($a + (int) $b);
Forced type casting makes your intention extremely clear to other developers who make use of your code. It also guarantees results in that forcing the type cast exerts greater control over the flow of your code, and does not rely upon PHP default behavior.
The FFI extension has a similar capability in the form of the FFI::cast() method. As you have seen throughout this chapter, FFI extension data is isolated from PHP and is immune to PHP type casting. In order to force the data type, you can use FFI::cast() to return a parallel FFICData type as required. Let's see how to do that in the following steps:
// /repo/ch04/php8_ffi_cast.php
// not all lines are shown
$patt = "%2d : %16s ";
$int1 = FFI::new("int");
$int1->cdata = 123;
$bool = FFI::cast(FFI::type("bool"), $int1);
printf($patt, __LINE__, (string) $int1->cdata);
printf($patt, __LINE__, (string) $bool->cdata);
8 : 123
9 : 1
$int2 = FFI::new("int");
$int2->cdata = 123;
$float1 = FFI::cast(FFI::type("float"), $int2);
$int3 = FFI::cast(FFI::type("int"), $float1);
printf($patt, __LINE__, (string) $int2->cdata);
printf($patt, __LINE__, (string) $float1->cdata);
printf($patt, __LINE__, (string) $int3->cdata);
15 : 123
16 : 1.7235971111195E-43
17 : 123
try {
$float2 = FFI::new("float");
$float2->cdata = 22/7;
$char1 = FFI::cast(FFI::type("char[20]"),
$float2);
printf($patt, __LINE__, (string) $float2->cdata);
printf($patt, __LINE__, (string) $char1->cdata);
} catch (Throwable $t) {
echo get_class($t) . ':' . $t->getMessage();
}
FFIException:attempt to cast to larger type
In this section, we addressed a series of FFI extension methods that create FFI extension object instances, compare values, gather information, and work with the C data infrastructure created. You learned there are FFI extension methods that mirror these same capabilities in the native PHP language. In the next section, we review a practical example that incorporates a C-function library into a PHP script using the FFI extension.
Any shared C library (generally with a *.so extension) can be included in a PHP application using the FFI extension. If you plan to work with any of the core PHP libraries or libraries produced when PHP extensions are installed, it's important to note that you have the ability to modify the behavior of the PHP language itself.
Before we examine how that works, let's first have a look at incorporating an external C library into a PHP script using the FFI extension.
For the purposes of illustration, we use a simple function that might have originated from a Computer Science 101 (CS101) class: the famous bubble sort. This algorithm is widely used in beginner's computer science classes because it's easy to follow.
Important note
The bubble sort is an extremely inefficient sort algorithm and has long been superseded by faster sorting algorithms such as the shell sort, quick sort, or merge sort algorithms. Although there is no authoritative reference for the bubble-sort algorithm, you can read a good general discussion of it here: https://en.wikipedia.org/wiki/Bubble_sort.
In this subsection, we do not go through details of the algorithm. Rather, the purpose of this subsection is to demonstrate how to take an existing C library and incorporate one of its functions into a PHP script. We now show you the original C source code, how to convert it into a shared library, and—finally—how to incorporate the library into PHP using FFI. Here's what we'll do:
#include <stdio.h>
void bubble_sort(int [], int);
void bubble_sort(int list[], int n) {
int c, d, t, p;
for (c = 0 ; c < n - 1; c++) {
p = 0;
for (d = 0 ; d < n - c - 1; d++) {
if (list[d] > list[d+1]) {
t = list[d];
list[d] = list[d+1];
list[d+1] = t;
p++;
}
}
if (p == 0) break;
}
}
gcc -c -Wall -Werror -fpic bubble.c
gcc -shared -o libbubble.so bubble.o
// /repo/ch04/php8_ffi_using_func_from_lib.php
function show($label, $arr, $max)
{
$output = $label . " ";
for ($x = 0; $x < $max; $x++)
$output .= $arr[$x] . ',';
return substr($output, 0, -1) . " ";
}
$bubble = FFI::cdef(
"void bubble_sort(int [], int);",
"./libbubble.so");
$max = 16;
$arr_b = FFI::new('int[' . $max . ']');
for ($i = 0; $i < $max; $i++)
$arr_b[$i]->cdata = rand(0,9999);
echo show('Before Sort', $arr_b, $max);
$bubble->bubble_sort($arr_b, $max);
echo show('After Sort', $arr_b, $max);
Before Sort
245,8405,8580,7586,9416,3524,8577,4713,
9591,1248,798,6656,9064,9846,2803,304
After Sort
245,304,798,1248,2803,3524,4713,6656,7586,
8405,8577,8580,9064,9416,9591,9846
Now that you have an idea how to integrate an external C library into a PHP application using the FFI extension, we turn to our last topic: PHP callbacks.
As we mentioned at the beginning of this section, it's possible to use the FFI extension to incorporate shared C libraries that are part of the actual PHP language (or its extensions). This integration is important as it allows you to read and write native PHP data in your C library by accessing the C data structures defined in the PHP shared C libraries.
The purpose of this subsection, however, is not to show you how to create a PHP extension. Rather, in this subsection, we introduce you to the FFI extension's ability to override native PHP language functionality. This ability is referred to as a PHP callback. Before we get into the implementation details, we must first examine potential dangers associated with this ability.
It's important to understand that the C functions defined in the various PHP shared libraries are often used by multiple PHP functions. Accordingly, if you override one of the low-level functions at the C level, you might experience unexpected behavior in your PHP application.
Another known issue is that overriding native PHP C functions has a high probability of producing memory leaks. Over time, a long-running application that uses such overrides can fail, and can potentially crash the server!
A final consideration is that the PHP callback capability is not supported on all FFI platforms. Accordingly, although the code might work on a Linux server, it might not work (or might not work the same) on a Windows server.
Tip
Rather than using a FFI PHP callback to override native PHP C library functionality, it might be easier, faster, and safer to just define your own PHP function!
Now that you have an idea of the dangers involved using PHP callbacks, let's have a look at a sample implementation.
In the following example, the zend_write internal PHP shared library C function is overridden using a callback that adds a line feed (LF) to the end of the output. Note that this override affects any native PHP function dependent upon it, including echo, print, printf: in other words, any PHP function that produces direct output. To implement a PHP callback, follow these steps:
// /repo/ch04/php8_php_callbacks.php
$zend = FFI::cdef("
typedef int (*zend_write_func_t)(
const char *str,size_t str_length);
extern zend_write_func_t zend_write;
");
echo "Original echo command does not output LF: ";
echo 'A','B','C';
echo 'Next line';
Original echo command does not output LF:
ABCNext line
$orig_zend_write = clone $zend->zend_write;
$zend->zend_write = function($str, $len) {
global $orig_zend_write;
$ret = $orig_zend_write($str, $len);
$orig_zend_write(" ", 1);
return $ret;
};
echo 'Revised echo command adds LF:';
echo 'A','B','C';
Revised echo command adds LF:
A
B
C
It's also important to note that modifying the PHP library C-language zend_write function has an impact on all PHP native functions using this C-language function. This includes print(), printf() (and its variants), and so forth.
This concludes our discussion of using the FFI extension in a PHP application. You now know how to incorporate native C functions from an external shared library. You also know how to substitute a PHP callback for a native PHP core or extension shared library, giving you the potential to alter the behavior of the PHP language itself.
In this chapter, you learned about the FFI, its history, and how it can be used to facilitate rapid PHP extension prototyping. You also learned that although the FFI extension should not be used to improve speed, it also serves the purpose of allowing your PHP application to directly call native C functions from an external C library. The power of this ability was demonstrated through an example that called a bubble-sort function from an external C library. This same capability can be extended to encompass any of the thousands of C libraries available, including machine learning, optical character recognition, communications, encryption; ad infinitum.
In this chapter, you acquired a deeper understanding of how PHP itself operates at the C- language level. You learned how to create and directly use C-language data structures, giving you the ability to interact, and even override, the PHP language itself. In addition, you now have an idea how to incorporate the functionality of any C-language library directly into a PHP application. A further benefit of this knowledge is that it serves to enhance your career prospects if you find a job with a company that either plans to develop, or has already developed, its own custom PHP extension.
The next chapter marks the beginning of a new section of the book, PHP 8 Tricks. In the next section, you will learn about backward-compatibility issues when upgrading to PHP 8. The next chapter specifically addresses backward-compatibility issues with respect to OOP.
3.236.171.68