Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for record with callbacks? #177

Closed
hakonhagland opened this issue Aug 23, 2019 · 10 comments
Closed

Add support for record with callbacks? #177

hakonhagland opened this issue Aug 23, 2019 · 10 comments
Labels
🐣Enhancement Things that make it work better

Comments

@hakonhagland
Copy link
Contributor

hakonhagland commented Aug 23, 2019

I have this C program using the GNU scientific library (the library can be installed on Ubuntu from package libgsl-dev):

#include <stdio.h>
#include <gsl/gsl_errno.h>
#include <gsl/gsl_math.h>
#include <gsl/gsl_roots.h>

double
my_f (double x, void *params)
{
    return x * x * x;
}

double
my_df (double x, void *params)
{
    return 3.0 * x * x;
}

void
my_fdf (double x, void *params, 
               double *y, double *dy)
{
    *y = x * x * x;
    *dy = 3.0 * x * x;
}

int main (void)
{
    gsl_root_fdfsolver *s = gsl_root_fdfsolver_alloc ( gsl_root_fdfsolver_newton );
    gsl_function_fdf FDF;
    FDF.f = &my_f;
    FDF.df = &my_df;
    FDF.fdf = &my_fdf;
    FDF.params = NULL;
    double initial_guess = 5.0;
    gsl_root_fdfsolver_set (s, &FDF, initial_guess);

    for (int i = 1; i <= 20; i++ ) {
        gsl_root_fdfsolver_iterate (s);
        double root = gsl_root_fdfsolver_root (s);
        printf( "%.8f\n", root);
    }
    gsl_root_fdfsolver_free (s);
    return 0;
}

The above can be compiled using gcc -Wall -o solve solve.c -lgsl -lgslcblas -lm and produces output:

$ ./solve
3.33333333
2.22222222
1.48148148
0.98765432
0.65843621
0.43895748
0.29263832
0.19509221
0.13006147
0.08670765
0.05780510
0.03853673
0.02569116
0.01712744
0.01141829
0.00761219
0.00507480
0.00338320
0.00225546
0.00150364

I started implementing this in Perl using FFI::Platypus:

package GSL::RootSolver::GslFunctionFDF;
use FFI::Platypus::Record;
record_layout(
    qw [
  (double,opaque)->double               f 
  (double,opaque)->double               df 
  (double,opaque,opaque,opaque)->double fdf 
  opaque                                params
   ]
);

package main;
use feature qw(say);
use strict;
use warnings;

use FFI::Platypus;
use FFI::CheckLib;
my $ffi = FFI::Platypus->new();
$ffi->lib( find_lib_or_exit( lib => 'gsl' ) );

$ffi->type('opaque'  => 'gsl_root_fdfsolver_type_ptr');
$ffi->type('opaque => 'gsl_root_fdfsolver');
$ffi->attach(
    'gsl_root_fdfsolver_alloc',
    [ 'gsl_root_fdfsolver_type_ptr' ] => 'gsl_root_fdfsolver'
);
my $solver_type = $ffi->cast(
    'opaque' => 'opaque*', 
    $ffi->find_symbol('gsl_root_fdfsolver_newton')
);
my $solver = gsl_root_fdfsolver_alloc( $$solver_type );
my $fdf = GSL::RootSolver::GslFunctionFDF->new();
my $f_func = $ffi->closure(
    sub {
        my( $x, $params ) = @_;
        return $x*$x*$x;
    }
);
$fdf->f( $f_func );
my $df_func = $ffi->closure(
    sub {
        my( $x, $params ) = @_;
        return 3 * $x * $x;
    }
);
$fdf->df( $df_func );
my $fdf_func = $ffi->closure(
    sub {
        my( $x, $params, $y, $dy ) = @_;
        $$y = $x * $x * $x;
        $$dy = 3.0 * $x * $x;
    }
);
$fdf->fdf( $fdf_func );
$fdf->params( undef );

When I run this:

$ p.pl
type not supported ((double,opaque)->double f) at ./p.pl line 6.

So callbacks in records is not implemented yet? Would it be difficult to add support for callbacks passed in records like this?

@plicease plicease added the 🐣Enhancement Things that make it work better label Aug 24, 2019
@plicease
Copy link
Member

plicease commented Aug 24, 2019

yeah, atm closures aren't supported as record members. However, you can use the opaque type instead and cast the closure value to opaque:

$fdf->f($ffi->cast( '(double,opaque)->double' => 'opaque', $f_func));

It would be nice to have broader support for these and other types for the record type though, so I'll keep this open so that it can be worked out when there is time.

@plicease
Copy link
Member

Keep in mind that you need to make sure that $f_func needs to stay in scope (somewhere) as long as it could be called from C space.

@hakonhagland
Copy link
Contributor Author

hakonhagland commented Aug 24, 2019

Thanks! The cast seems to work fine for $fdf->f but for $fdf->fdf I would need an opaque* (and not an opaque) as input argument (I think ?):

$fdf->fdf(
    $ffi->cast( '(double,opaque,opaque*,opaque*)->double' => 'opaque', $fdf_func)
);

But this gives error:

Only native types and strings are supported as closure argument types (68) at /home/hakon/perlbrew/perls/perl-5.28.1/lib/site_perl/5.28.1/x86_64-linux/FFI/Platypus/TypeParser/Version0.pm line 67.

@plicease
Copy link
Member

But this gives error:

Only native types and strings are supported as closure argument types (68) at /home/hakon/perlbrew/perls/perl-5.28.1/lib/site_perl/5.28.1/x86_64-linux/FFI/Platypus/TypeParser/Version0.pm line 67.

right closures have a limited support for types. Some types like record couldn't be supported the way they are currently implemented. Others are tricky because of memory ownership (returning a string from a closure probably won't ever be supported). Pointer types probably could be supported in the future, but it hasn't been a priority so far.

Most of these thing can be dealt with using a cast inside the closure though.

# closure that takes an opaque* as an argument
$ffi->closure(sub {
  my($a, $b, $c, $d) = @_;
  $c = $ffi->cast('opaque' => 'opaque*', $c);
  ...
});

# closure that returns a string
$ffi->closure(sub {
  ...
  state $ret;  # cannot be my
  $ret = ...;
  return $ffi->cast('string' => 'opaque', $ret);
});

@hakonhagland
Copy link
Contributor Author

hakonhagland commented Aug 24, 2019

Interesting! Ok so if I try:

my $fdf_func = $ffi->closure(
    sub {
        my( $x, $params, $y, $dy ) = @_;
        $y = $ffi->cast('opaque' => 'opaque*', $y);
        $dy = $ffi->cast('opaque' => 'opaque*', $dy);
        $$y = $x * $x * $x;
        $$dy = 3.0 * $x * $x;
    }
);

I now get error:

Modification of a read-only value attempted at ./p.pl line 58.

where line no 58 is this:

        $$y = $x * $x * $x;

@plicease
Copy link
Member

plicease commented Aug 29, 2019

What you want is to write a double to an arbitrary location of memory which Perl doesn't allow, but you can use the C memcpy function. Something like: (not tested)

 # (double, opaque, opaque, opaque)->void
my $fdf_func = $ffi->closure(
    sub {
        my( $x, $params, $y, $dy ) = @_;
        my $memcpy = $ffi->function( memcpy => [ 'opaque', 'double*', 'size_t']=> 'opaque');
        my $size = $ffi->sizeof('double');
        $memcpy->($y, \($x*$x*$x), $size);
        $memcpy->($dy, \(3.0*$x*$x), $size);
    }
);

(of course if you expect this to be called frequently, which presumably it will, you will want to attach memcpy and save the sizeof value.

@hakonhagland
Copy link
Contributor Author

hakonhagland commented Aug 30, 2019

Wow! This actually works now:

package GSL::RootSolver::GslFunctionFDF;
use FFI::Platypus::Record;

record_layout(
    qw [
  opaque        f 
  opaque        df 
  opaque        fdf 
  opaque        params
   ]
);

package main;
use feature qw(say);
use strict;
use warnings;

use Data::Printer;
use FFI::Platypus;
use FFI::CheckLib;
my $ffi = FFI::Platypus->new();
$ffi->lib( find_lib_or_exit( lib => 'gsl' ) );

$ffi->type('opaque'       => 'gsl_root_fdfsolver_type_ptr');
$ffi->type('opaque'     => 'gsl_root_fdfsolver');
$ffi->attach(
    'gsl_root_fdfsolver_alloc',
    [ 'gsl_root_fdfsolver_type_ptr' ] => 'gsl_root_fdfsolver'
);
my $solver_type = $ffi->cast(
    'opaque' => 'opaque*', 
    $ffi->find_symbol('gsl_root_fdfsolver_newton')
);
my $solver = gsl_root_fdfsolver_alloc( $$solver_type );
my $fdf = GSL::RootSolver::GslFunctionFDF->new();
my $f_func = $ffi->closure(
    sub {
        my( $x, $params ) = @_;
        return $x*$x*$x;
    }
);
$fdf->f($ffi->cast( '(double,opaque)->double' => 'opaque', $f_func) );
my $df_func = $ffi->closure(
    sub {
        my( $x, $params ) = @_;
        return 3 * $x * $x;
    }
);
$fdf->df($ffi->cast( '(double,opaque)->double' => 'opaque', $df_func) );
my $fdf_func = $ffi->closure(
    sub {
        my( $x, $params, $y, $dy ) = @_;
        my $memcpy = $ffi->function(
            memcpy => [ 'opaque', 'double*', 'size_t']=> 'opaque'
        );
        my $size = $ffi->sizeof('double');
        $memcpy->($y, \($x*$x*$x), $size);
        $memcpy->($dy, \(3.0*$x*$x), $size);
    }
);
$fdf->fdf(
    $ffi->cast( '(double,opaque,opaque,opaque)->double' => 'opaque', $fdf_func)
);

$fdf->params( undef );
my $initial_guess = 5.0;
$ffi->type("record(GSL::RootSolver::GslFunctionFDF)" => 'gsl_function_fdf');
$ffi->attach(
    'gsl_root_fdfsolver_set',
    [ 'gsl_root_fdfsolver', 'gsl_function_fdf', 'double' ] => 'gsl_root_fdfsolver'
);
$ffi->attach(
    'gsl_root_fdfsolver_iterate', [ 'gsl_root_fdfsolver' ] => 'int'
);
$ffi->attach(
    'gsl_root_fdfsolver_root', [ 'gsl_root_fdfsolver' ] => 'double'
);
$ffi->attach(
    'gsl_root_fdfsolver_free', [ 'gsl_root_fdfsolver' ] => 'void'
);

gsl_root_fdfsolver_set ($solver, $fdf, $initial_guess);

for my $i (1..20) {
    gsl_root_fdfsolver_iterate ($solver);
    my $root = gsl_root_fdfsolver_root ($solver);
    printf "%.8f\n", $root;
}
gsl_root_fdfsolver_free ($solver);

Output

3.33333333
2.22222222
1.48148148
0.98765432
0.65843621
0.43895748
0.29263832
0.19509221
0.13006147
0.08670765
0.05780510
0.03853673
0.02569116
0.01712744
0.01141829
0.00761219
0.00507480
0.00338320
0.00225546
0.00150364

@plicease
Copy link
Member

I am pretty sure we can support record pass-by-value into a closure, but not pointer to a record (at least based on the current record implementation).

@plicease
Copy link
Member

plicease commented Mar 3, 2021

I am pretty sure we can support record pass-by-value into a closure, but not pointer to a record (at least based on the current record implementation).

Revisiting some old tickets here and using structs from FFI::C you can pretty easily write a closure that takes a struct as an argument, but you have to cast it from an opaque pointer. It would be good to:

  1. Document this
  2. Extend the type plugin interface so that you can do this automatically

@plicease
Copy link
Member

#312 adds support for passing a record into a closure. Passing a pointer into a closure already worked, but due to limitations in the record interface, that is actually a copy too, so records can only be passed into a closure by-value, and they are read-only.

For passing a pointer to a record into a closure it makes more sense for the closure to take a opaque and cast that into a FFI::C object, which you can then inspect and even modify. If #304 happens this will be possible to do without a cast. But that is down the road.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🐣Enhancement Things that make it work better
Development

No branches or pull requests

2 participants