Getting size of buffer in Perl via FFI::Platypus

Viewed 91

I'm using FFI::Platypus to call a C function. The function takes four arguments: two strings, a pointer to a buffer, and the buffer size:

int helper_getAddr(
    const char *firstName,
    const char *lastName,
    char **address,
    size_t *addressLen);

I'm defining it like this via FFI::Platypus:

$self->{ffi}->attach( [helper_getAddr => 'get_addr']
    => ['string', 'string', 'string', 'size_t'] => 'int' );

And then calling like so:

my $contents_ptr = malloc 100;
my $size;
my $success = get_addr( "Tom", "Baker", \$contents_ptr, \$size );

So I can use buffer_to_scalar to get the buffer contents.

The call appears to work - $success = 0 and $contents_ptr is defined - but $size is not being set.

I've worked very little with C/C++, so I'm not sure if my issue is in the declaration of $size, or if I need to change my call to get_addr().

Alternatively, is there a way I can use $contents_ptr directly in Perl to find the length of the buffer before I call buffer_to_scalar?

1 Answers

The following is incorrect:

[ 'string', 'string', 'string', 'size_t' ]
  • The third arg of helper_getAddr isn't a string.
  • The fourth arg of helper_getAddr isn't a size_t.

Before continuing, we must determine what the function expects. This is problematic, because the function's arguments don't make much sense.

If helper_getAddr populates an existing buffer, I would expect

int helper_getAddr(
   const char *firstName,
   const char *lastName,
   char **address,
   size_t *addressLen
);

If helper_getAddr allocates and returns a string, I would expect

char *helper_getAddr(
   const char *firstName,
   const char *lastName
);

or

int helper_getAddr(
   const char *firstName,
   const char *lastName,
   char **address
);

Aside from having an extra useless argument, helper_getAddr most closely resembles a function that allocates and returns a string. So I'm going to proceed under the assumption that helper_getAddr allocates and returns a string.


Given the function we have, we'd be tempted to use the following:

$ffi->attach(
   [ 'helper_getAddr' => '_get_addr' ],
   [ 'string', 'string', 'string*', 'size_t*' ],    # XXX
   'int',
);

The problem with that is that it doesn't give us access to the pointer we need to free. As such, we shall use

$ffi->attach(
   [ 'helper_getAddr' => '_get_addr' ],
   [ 'string', 'string', 'opaque*', 'size_t*' ],
   'int',
);

A simple wrapper will give the function a Perl-ish appearance and handle freeing the buffer.

sub get_addr {
   _get_addr($_[0], $_[1], \my $buf, \my $buf_size)
      or return undef;

   my $addr = $ffi->cast('opaque' => 'string', $buf);
   free($buf);
   return $addr;
}
  • We don't call malloc in Perl because the function will simply overwrite the pointer, resulting in a memory leak.
  • By casting the buffer to the string type, Platypus::FFI will create a Perl string from the NUL-terminated contents of the returned buffer, so there's no need for buffer_to_scalar either.

Example usage:

say get_addr("Tom", "Baker") // "[undef]";

Full solution follows.


lookup

#!/home/ikegami/usr/perlbrew/perls/5.32.0t/bin/perl

use strict;
use warnings;
use feature qw( say state );

use FindBin qw( $RealBin );

use FFI::Platypus         qw( );
use FFI::Platypus::Memory qw( free );

my $ffi = FFI::Platypus->new( api => 1 );
$ffi->find_lib(
   lib     => 'lookup',
   libpath => $RealBin,
);

$ffi->attach(
   [ 'get_addr' => '_get_addr' ],
   [ 'string', 'string', 'opaque*', 'size_t*' ],
   'int',
);

sub get_addr {
   _get_addr($_[0], $_[1], \my $buf, \my $buf_size)
      or return undef;

   my $addr = $ffi->cast('opaque' => 'string', $buf);
   free($buf);
   return $addr;
}

say get_addr("Tom", "Baker") // "[undef]";

liblookup.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int get_addr(
   const char *first_name,
   const char *last_name,
   char **address_ptr,
   size_t *address_size_ptr
) {
   const char *prefix = "Address of ";

   const size_t len_prefix = strlen(prefix);
   const size_t len_f_n    = strlen(first_name);
   const size_t len_l_n    = strlen(last_name);

   *address_size_ptr = len_prefix + len_f_n + 1 + len_l_n + 1;
   *address_ptr = malloc(*address_size_ptr);
   if (!*address_ptr)
      return 0;

   char *p = *address_ptr;
   memmove(p, prefix, len_prefix);   p += len_prefix;
   memmove(p, first_name, len_f_n);  p += len_f_n;
   *p = ' ';                         ++p;
   memmove(p, last_name, len_l_n);   p += len_l_n;
   *p = 0;                           ++p;

   return 1;
}

run

#!/bin/bash
trap 'printf '\''error!\n'\''; exit 1' ERR

script="$( readlink -e -- "$0" )"
script_dir="$( dirname -- "$script" )"
home_dir="$script_dir"
cd "$home_dir"

prog=./lookup

# Use the PATH to locate the program.
prog="$( which -- "$prog" )"

# Use the program's shebang to locate the appropriate perl.
perl="$( perl -ne'chomp; print s/^#!//r; exit;' "$prog" )"

# Extract compiler and linker information from the correct perl.
get_config() { "$perl" -MConfig -e'print $Config{$ARGV[0]}' "$1"; }
cc="$( get_config cc )"
ccflags="$( get_config ccflags )"
optimize="$( get_config optimize )"
cccdlflags="$( get_config cccdlflags )"
ld="$( get_config ld )"
lddlflags="$( get_config lddlflags )"

# Build the shared library.
"$cc" -c $ccflags $optimize $cccdlflags liblookup.c -o liblookup.o
"$ld" $lddlflags liblookup.o -o liblookup.so

# Run our test.
"$prog"
Related