phparraysctypesffi

PHP FFI - Convert PHP array to C pointers array


I have a C function with the following signature. It accepts (from my humble C understanding) an array of C strings and returns a pointer to a random string.

const char *get_random(const char *const *words, uintptr_t len);

What I'm doing is testing this function in different languages using FFI, for something like Java it works pretty simple (with the help of JNA library):

String[] words = { "One", "Two", "Three" };
String word = MyFFI.INSTANCE.get_random(words, words.length);

However I struggle to convert PHP array to the suitable type using FFI API due to poor understanding of C:

$ffi_def = file_get_contents("./get_random.h");
$ffi = FFI::cdef($ffi_def, "./libget_random.so");

$words = ["One", "Two", "Three"];

// Should call `$ffi->get_random()` here

I tried a couple of options using FFI API, but ended up with a bunch of errors like SIGSEGV (Address boundary error) and Attempt to perform assign pointer to owned C data. I won't share examples cause they are just some gibberish.

If anyone could help me here or at least push me in the right direction?


Solution

  • PHP is a dynamically typed language and doesn't offer a way of representing a strict string[] data type out of the box like Java does. So you'd need to construct it in FFI first in order to pass the function the data type is expects. For this purpose we'd reach for FFI::new.

    // Note: Before you use this as your final solution, read the rest of the post.
    // Assuming it's const char *get_random(const char *const *words, uintptr_t len);
    $ffi_def = file_get_contents("./get_random.h");
    $ffi = FFI::cdef($ffi_def, "./libget_random.so");
    
    $words = ['One', 'Two', 'Three'];
    
    $count = count($words);
    
    $cWords = FFI::new("char *[$count]", false);
    foreach ($words as $i => $word) {
        $len = strlen($word);
        $cWords[$i] = FFI::new("char[$len]", false);
        FFI::memcpy($cWords[$i], $word, $len);
    }
    
    $randomWord = $ffi->get_random($cWords, $count);
    
    for ($i = 0; $i < $count; $i++) {
        FFI::free($cWords[$i]);
    }
    FFI::free($cWords);
    
    echo "$randomWord\n";
    

    The above approach is based on code from over here:
    https://github.com/dstogov/php-ffi/issues/40

    Note this approach passes FFI::new a falsey value so we have to manage the memory ourselves. This has the disadvantage of not being able to use PHP's garbage collecting. If you swap it for "true", you'll run into some errors without adjusting the code. Read on! I'll show you how to do just that.

    Another approach would be to adjust the dynamic library so it accepts a format easier for PHP to populate. A simple example might be using a comma as a separator. For example:

    <?php
    // Assuming it's changed to: char *get_random(char *words, uintptr_t len);
    $ffi_def = file_get_contents('./get_random.h');
    $ffi = FFI::cdef($ffi_def, './libget_random.so');
    
    $words = ['One', 'Two', 'Three'];
    
    $str = implode(',', $words);
    
    $randomWord = $ffi->get_random($str, strlen($str));
    
    echo FFI::string($randomWord), "\n";
    

    Note that in the above, instead of representing the number of elements in the array, I changed the second parameter to now represent the string length. Then basically write your C so it splits on the comma character, probably using strtok_r. If your string can contain a comma, maybe base64 around it or something. Json is another option, but it might be too powerful since we don't want to support multiple dimensions for merely dealing with a string[].

    As you can see from here, you can pass strings ok. The problem is just PHP doesn't have a strict string[] data type. If it did, you could pass it in directly.

    For example, in your C, you could write something like:

    char *globalStringArray[] = { "One", "Two", "Three" };
    

    Then in the header add:

    char *globalStringArray[];
    

    And you can use it like so:

    <?php
    $ffi_def = file_get_contents('./get_random.h');
    $ffi = FFI::cdef($ffi_def, './libget_random.so');
    $randomWord = $ffi->get_random($ffi->globalStringArray, 3);
    echo "$randomWord\n";
    

    This is a proof of concept that if you can get a strict string[] loaded, you can pass it directly into the function.

    Another approach would be adding more methods to the dynamic library which would be easier for PHP to work with:

    <?php
    $ffi_def = file_get_contents('./get_random.h');
    $ffi = FFI::cdef($ffi_def, './libget_random.so');
    
    $deck = $ffi->newDeck();
    $ffi->addCard($deck, 'One');
    $ffi->addCard($deck, 'Two');
    $ffi->addCard($deck, 'Three')
    
    $card = $ffi->getRandomCard($deck);
    $ffi->freeDeck($deck);
    
    echo "$card\n";
    

    Now that I introduced these other options, you're probably curious about the best approach. Yes, that would probably be FFI::new. But no, we probably don't want to pass it a falsey second argument if we can get this to work using PHP's garbage collecting.

    Ready for the solution? After playing around with this I found that we basically need to cast to avoid an ownership error. And when assigning, we need to keep the variable in scope so PHP's garbage collecting doesn't kick in. When you're in a foreach loop, the variable will go out of scope bewteen loops unless it exists outside of the scope of the loop.

    VoilĂ !

    <?php
    // Assuming: const char *get_random(const char *const *words, uintptr_t len);
    $ffi_def = file_get_contents('./get_random.h');
    $ffi = FFI::cdef($ffi_def, './libget_random.so');
    
    $words = ['One', 'Two', 'Three'];
    
    $wordsCount = count($words);
    $cWords = $ffi->new("char*[$wordsCount]");
    
    // Need to keep this in memory so PHP's garbage collecting doesn't kick in between loops
    $cWordsHolder = [];
    
    foreach($words as $i => $word)
    {
        $len = strlen($word);
        $cWordsHolder[$i] = $ffi->new("char[$len]");
        FFI::memcpy($cWordsHolder[$i], $word, $len);
        $cWords[$i] = FFI::cast('char*', $cWordsHolder[$i]); // cast needed to avoid ownership error
    }
    
    $chosenString = $ffi->get_random($cWords, $wordsCount);
    echo "$chosenString\n"; 
    

    A finishing touch I'd do is swap over to object-oriented programming so the entire dynamic library feels like an object:

    <?php
    
    class GetRandomFFI
    {
        protected FFI $ffi;
        protected array $memoryHolder = [];
        public function __construct()
        {
            $ffi_def = file_get_contents(__DIR__ . '/get_random.h');
            $this->ffi = FFI::cdef($ffi_def, __DIR__ . '/libget_random.so');
        }
    
        public function getRandom(array $words = []): ?string
        {
            if (!count($words)) {
                return null;
            }
            $output = $this->ffi->get_random($this->phpStringArrayToCArray($words), count($words));
            // Since we used our argument, we can clear it from memory early now, but not needed unless we're gonna do a lot of heavy-lifting.
            // Once our class instance goes out of scope, this would clear automatically. Might as well clear it early.
            array_pop($this->memoryHolder);
            return $output;
    
        }
        protected function phpStringArrayToCArray(array $phpStringArray)
        {
            $phpStringArrayCount = count($phpStringArray);
            $cArray = $this->ffi->new("char*[$phpStringArrayCount]");
            $memoryHolder = [];
            foreach($phpStringArray as $i => $phpString)
            {
                $len = strlen($phpString);
                $memoryHolder[$i] = $this->ffi->new("char[$len]");
                FFI::memcpy($memoryHolder[$i], $phpString, $len);
                $cArray[$i] = FFI::cast('char*', $memoryHolder[$i]); // cast needed to avoid ownership error
            }
            // Once this goes out of scope, our $cArray will point to garbage.
            // So we store this on a property that will live as long as our class instance.
            $this->memoryHolder[] = $memoryHolder;
            return $cArray;
        }
    }
    
    
    $grf = new GetRandomFFI();
    echo $grf->getRandom(['One','Two','Three']), "\n";