Chapter 4: Making Direct C-Language Calls

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:

  • Understanding FFI
  • Learning where to use FFI
  • Examining the FFI class
  • Using FFI in an application
  • Working with PHP callbacks

Technical requirements

To examine and run the code examples provided in this chapter, the minimum recommended hardware is listed here:

  • X86_64-based desktop PC or laptop
  • 1 gigabyte (GB) free disk space
  • 4 GB of random-access memory (RAM)
  • 500 kilobits per second (Kbps) or faster internet connection

In addition, you will need to install the following software:

  • Docker
  • Docker Compose

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.

Understanding 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.

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.

Understanding PHP extensions

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.

Accessing PHP 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:

Figure 4.1 – PHP core extensions seen on GitHub

Figure 4.1 – PHP core extensions seen on GitHub

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.

Examining non-core PHP extensions

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.

Finding 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:

Figure 4.2 – pecl.php.net page for the PHP MongoDB extension

Figure 4.2 – pecl.php.net page for the PHP MongoDB 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.

Learning where to 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.

Adopting FFI 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.

Do not use FFI for 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.

Why use the FFI extension?

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.

Examining the FFI class

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:

  • Creational: Methods in this category create instances of classes available from the FFI extension application programming interface (API).
  • Comparison: Comparison methods are designed to compare C data values.
  • Informational: This set of methods gives you metadata on C data values, including size and alignment.
  • Infrastructural: Infrastructural methods are used to carry out logistical operations such as copying, populating, and releasing memory.

    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.

Working with FFI 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:

Table 4.1 – Summary of FFI class creational methods

Table 4.1 – Summary of FFI class creational methods

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.

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:

  1. First, we create the array using FFI::arrayType(). As arguments, we supply a FFI::type() method and dimensions. We then use FFI::new() to create the FFICtype instance. The code is illustrated in the following snippet:

    // /repo/ch04/php8_ffi_array.php

    $type = FFI::arrayType(FFI::type("char"), [3, 3]);

    $arr  = FFI::new($type);

  2. Alternatively, we could also combine the operations into a single statement, as shown here:

    $arr = FFI::new(FFI::type("char[3][3]"));

  3. We then initialize three variables that provide test data, as shown in the following code snippet. Note that the native PHP count() function works on FFICData array types:

    $pos   = 0;

    $val   = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';

    $y_max = count($arr);

  4. We can now populate it with values, much as with a PHP array, except that we need to use the cdata property in order to retain the element as a FFICType instance. The code is shown in the following snippet:

    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.

Creating and using 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:

  1. The first native method, srand(), is used to seed a randomization sequence. rand(), the other native C function, calls the next number in the sequence. The $key variable holds the final product of the randomization. $size represents the number of random numbers to call. The code is illustrated in the following snippet:

    // /repo/ch04/php8_ffi_cdef.php

    $key  = '';

    $size = 4;

  2. We then create the FFI instance by invoking cdef() and identifying the native C functions in a string $code, taken out of the libc.so.6 native C library, as follows:

    $code = <<<EOT

        void srand (unsigned int seed);

        int rand (void);

    EOT;

    $ffi = FFI::cdef($code, 'libc.so.6');

  3. We then seed the randomization by calling srand(). Then, in a loop, we invoke the rand() native C library function to produce a random number. We use the sprintf() native PHP function to convert the resulting integer to hex, the output of which is appended to $key, which is echoed. The code can be seen here:

    $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.

Comparing data using FFI

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:

Table 4.2 – Summary of FFI class comparison methods

Table 4.2 – Summary of FFI class comparison methods

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:

  1. We first define a set of four variables representing FFICData instances that can contain up to six characters and populate the instances with sample data, as follows:

    // /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]");

  2. Recall that the C language treats character data as an array, so we can't just directly assign a string, even if using the cdata property. Accordingly, we need to define an anonymous function that populates the instances with letters of the alphabet. We use the following code to do this:

    $populate = function ($cdata, $start, $offset, $num) {

        for ($x = 0; $x < $num; $x++)

            $cdata[$x + $offset] = chr($x + $offset +

                                       $start);

        return $cdata;

    };

  3. Next, we use the function to populate the four FFICData instances with differing sets of letters, as follows:

    $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);

  4. We can now use the FFI::string() method to display the contents thus far, as follows:

    $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));

  5. Here is the output from the printf() statements:

    $a : ABCDEF

    $b : ABCXYZ

    $c : GHIJKL

    $d : GHIJKL

  6. As you can see from the output, the values of $c and $d are the same. The first three characters for $a and $b are the same, but the last three are different.
  7. At this point, if we were to try to use the spaceship operator (<=>) for comparison, the result would be the following:

    PHP Fatal error:  Uncaught FFIException: Comparison of incompatible C types

  8. Likewise, an attempt to use strcmp(), even though the data is a character type, the result would be as follows:

    PHP Warning:  strcmp() expects parameter 1 to be string, object given

  9. Accordingly, our only alternative is to use FFI::memcmp(). In the set of comparisons shown here, note that the third argument is 6, indicating PHP should compare up to six characters:

    $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));

  10. As expected, the output is the same as using the spaceship operator on native PHP strings, as shown here:

       memcmp($a, $b, 6) : -1

       memcmp($c, $a, 6) :  1

       memcmp($c, $d, 6) :  0

  11. Note what happens if we restrict the comparison to only three characters. Here is another FFI::memcmp() comparison added to the code block, setting the third argument to 3:

    echo " Using FFI::memcmp() but not full length ";

    printf($p, 'memcmp($a, $b, 3)', FFI::memcmp($a,

            $b, 3));

  12. As you can see from the output shown here, by restricting memcmp() to only three characters, $a and $b are considered equal because they both start with the same three characters, a, b, and c:

    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.

Extracting 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:

Table 4.3 – Summary of FFI class informational methods

Table 4.3 – Summary of FFI class informational methods

We first look at FFI::typeof(), after which we dive into the other two methods.

Determining the nature of FFI data using FFI::typeof()

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:

  1. First, we define a $char C string and populate it with the first six letters of the alphabet, as follows:

    // /repo/ch04/php8_ffi_typeof.php

    $char = FFI::new("char[6]");

    for ($x = 0; $x < 6; $x++)

        $char[$x] = chr(65 + $x);

  2. We then attempt to use strlen() to get the length of the string. In the following code snippet, note the use of $t::class: this is the equivalent of get_class($t). This usage is only available in PHP 8 and above:

    try {

        echo 'Length of $char is ' . strlen($char);

    } catch (Throwable $t) {

        echo $t::class . ':' . $t->getMessage();

    }

  3. The result in PHP 7.4 is a Warning message. However, in PHP 8, if you pass anything other than a string to strlen(), a fatal Error message is thrown. Here is the PHP 8 output at this point:

    TypeError:strlen(): Argument #1 ($str) must be of type string, FFICData given

  4. In a similar manner, an effort to use ctype_alnum() is made, as follows:

    echo '$char is ' .

        ((ctype_alnum($char)) ? 'alpha' : 'non-alpha');

  5. Here is the output from the echo command shown in Step 4:

    $char is non-alpha

  6. As you can clearly see, we are not getting any useful information about the FFI data using native PHP functions! However, using FFI::typeof(), as shown here, returns better results:

    $type = FFI::typeOf($char);

    var_dump($type);

  7. Here is the output from var_dump():

    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.

Making use of FFI::alignof() and FFI::sizeof()

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:

  1. First, we create a FFI instance, $ffi, in which we define two C structures labeled Good and Bad. Notice in the following code snippet that both structures have the same properties; however, the properties are arranged in a different order:

    $struct = 'struct Bad { char c; double d; int i; }; '

            . 'struct Good { double d; int i; char c; };

              ';

    $ffi = FFI::cdef($struct);

  2. We then extract the two structures from $ffi, as follows:

    $bad = $ffi->new("struct Bad");

    $good = $ffi->new("struct Good");

    var_dump($bad, $good);

  3. The var_dump() output is shown here:

    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) ""

    }

  4. We then use the two informational methods to report on the two data structures, as follows:

    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.

Using FFI infrastructural methods

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:

Table 4.4 – FFI class infrastructural methods

Table 4.4 – FFI class infrastructural methods

Let's first have a look at FFI::addr(), free(), memset(), and memcpy().

Working with 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:

  1. First, a FFICData instance, $arr, is created, consisting of a C string of six characters. Note in the following code snippet the use of FFI::memset(), another infrastructural method, to populate the string with American Standard Code for Information Interchange (ASCII) code 65: the letter A:

    // /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);

  2. The echo result using the FFI::string() method is shown here:

    AAAAAA

  3. As you can see from the output, six instances of ASCII code 65 (the letter A) appears. We then create another FFICData instance, $arr2, and use FFI::memcpy() to copy six characters from one instance to the other, as follows:

    $arr2  = FFI::new(FFI::type("char[$size]"));

    FFI::memcpy($arr2, $arr, $size);

    echo FFI::string($arr2, $size);

  4. Unsurprisingly, the output is identical to the output in Step 2, as we can see here:

    AAAAAA

  5. Next, we create a C pointer to $arr. Note that when pointers are assigned, they appear to the native PHP var_dump() function as array elements. We can then change the value of array element 0, and use FFI::memset() to populate it with the letter B. The code is shown in the following snippet:

    $ref = FFI::addr($arr);

    FFI::memset($ref[0], 66, 6);

    echo FFI::string($arr, $size);

    var_dump($ref, $arr, $arr2);

  6. Here is the output associated with the remaining code shown in Step 5:

    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.

  7. Finally, in order to release the pointer created using FFI::addr(), we use FFI::free(). This method is much like the native PHP unset() function but is designed to work with C pointers. Here is the last line of code added to our example:

    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.

Learning about FFI::cast()

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:

  1. In this example, we create a FFICData instance, $int1, of type int. We use its cdata property to assign a value 123, as follows:

    // /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);

  2. As you can see from the output shown here, the integer value of 123, when typecast to bool (Boolean), shows up in the output as 1:

    8 :                  123

    9 :                    1

  3. Next we create a FFICData instance, $int2, of type int and assign a value 123. We then typecast it to float and back again to int, as illustrated in the following code snippet:

    $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);

  4. The output from the last three lines is quite gratifying. We see that our original value 123 is represented as 1.7235971111195E-43. When typecast back to int, our original value is restored. Here is the output from the last three lines:

    15 :                 123

    16 : 1.7235971111195E-43

    17 :                 123

  5. The FFI extension, as with the C language in general, does not allow all types to be converted. As an example, in the last block of code, we attempt to typecast a FFICData instance, $float2, of type float to type char, as follows:

    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();

    }

  6. The results are disastrous! As you can see from the output shown here, a FFIException is thrown:

    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.

Using FFI in an application

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.

Integrating an external C library into a PHP script

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:

  1. The first step, of course, is to compile the C code into object code. Here is the bubble-sort C code used for this example:

    #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;

        }

    }

  2. We then compile the C code into object code using the GNU C compiler (included in the Docker image used for this course), as follows:

    gcc -c -Wall -Werror -fpic bubble.c

  3. Next, we incorporate the object code into a shared library. This step is necessary as the FFI extension is only able to access shared libraries. We run the following code to do this:

    gcc -shared -o libbubble.so bubble.o

  4. We are now ready to define the PHP script that uses our new shared library. We begin by defining a function that shows output from a FFICData array, as follows:

    // /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) . " ";

    }

  5. The critical part is next: defining the FFI instance. We use FFI::cdef() to accomplish this and supply two arguments. The first argument is the function signature, and the second argument is a path to our newly created shared library. Both arguments can be seen in the following code snippet:

    $bubble = FFI::cdef(

        "void bubble_sort(int [], int);",

        "./libbubble.so");

  6. We then create a FFICData element as an integer array with 16 values populated with random integers, using the rand() function. The code is shown in the following snippet:

    $max   = 16;

    $arr_b = FFI::new('int[' . $max . ']');

    for ($i = 0; $i < $max; $i++)

        $arr_b[$i]->cdata = rand(0,9999);

  7. Finally, we display the contents of the array before the sort, perform the sort, and display the contents after. Note in the following code snippet that we execute the sort using a call to bubble_sort() from the FFI instance:

    echo show('Before Sort', $arr_b, $max);

    $bubble->bubble_sort($arr_b, $max);

    echo show('After Sort', $arr_b, $max);

  8. The output, as you might expect, shows an array of random integers before the sort. After the sort, the values are in order. Here is the output from the code shown in Step 7:

    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.

Working with 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.

Understanding the dangers inherent to PHP callbacks

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.

Implementing a PHP callback

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:

  1. First, we define a FFI instance using FFI::cdef(). The first argument is the function signature of zend_write. The code is shown in the following snippet:

    // /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;

    ");

  2. We then add code to confirm that, unmodified, echo does not add an extra LF at the end. You can see the code here:

    echo "Original echo command does not output LF: ";

    echo 'A','B','C';

    echo 'Next line';

  3. Unsurprisingly, the output produces ABCNext line. There are no carriage returns or LFs present in the output, which is shown here:

    Original echo command does not output LF:

    ABCNext line

  4. We then clone the pointer to zend_write into the $orig_zend_write variable. If we didn't do this, we would be unable to use the original function! The code is shown here:

    $orig_zend_write = clone $zend->zend_write;

  5. Next, we produce a PHP callback in the form of an anonymous function that overrides the original zend_write function. In the function, we invoke the original zend_write function and append a LF to its output, as follows:

    $zend->zend_write = function($str, $len) {

        global $orig_zend_write;

        $ret = $orig_zend_write($str, $len);

        $orig_zend_write(" ", 1);

        return $ret;

    };

  6. The remaining code reruns the echo command shown in the preceding step, as we can see here:

    echo 'Revised echo command adds LF:';

    echo 'A','B','C';

  7. The following output demonstrates that the PHP echo command now produces a LF at the end of each command:

    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.

Summary

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.236.171.68