Hello,
If this is not the correct list to report a user error, I apologize and will happily redirect my question where ever suitable.
I've been using cffi to make a trivial library to libsensors https://github.com/groeck/lm-sensors and am getting an odd memory fault error when referencing a string value from a groveled struct. After hitting my head against a wall for a day and a half, I thought I'd ask in case it's a simple mistake.
Here's my information:
Running a 64 bit linux install. My local version of the libsensors library is as follows:
elliott@desktop ~ $ file /usr/lib64/libsensors.so.4.4.0 /usr/lib64/libsensors.so.4.4.0: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, stripped elliott@desktop ~ $ sensors --version sensors version 3.4.0+git_83cafd29f28d463573750d897014ec7143217ae5 with libsensors version 3.4.0+git_83cafd29f28d463573750d897014ec7143217ae5
I'm using a fairly recent version of sbcl built from source that (apparently) passed all of the tests after the build:
CL-USER> (lisp-implementation-version) "1.4.6.140-f8d5864d0"
My version of cffi appears to be the latest:
CL-USER> (slot-value (asdf:find-system :cffi) 'asdf:version) "0.19.0"
My system uses cffi-grovel and defines the following structs:
(cstruct sensors-bus-id "sensors_bus_id" (type "type" :type :short) (nr "nr" :type :short))
(cstruct sensors-chip-name "sensors_chip_name" (prefix "prefix" :type :string) (bus "bus" :type (:struct sensors-bus-id)) (address "addr" :type :int) (path "path" :type :string))
These are based upon the definitions in sensors/sensors.h: https://github.com/groeck/lm-sensors/blob/master/lib/sensors.h
After asdf loading the system, the following groveler definitions are created (had to dig these out of the cached build files):
(cffi:defcstruct (sensors-bus-id :size 4) (type :short :offset 0) (nr :short :offset 2)) (cl:defconstant size-of-sensors-bus-id (cffi:foreign-type-size '(:struct sensors-bus-id))) (cffi:defcstruct (sensors-chip-name :size 24) (prefix :string :offset 0) (bus (:struct sensors-bus-id) :offset 8) (address :int :offset 12) (path :string :offset 16)) (cl:defconstant size-of-sensors-chip-name (cffi:foreign-type-size '(:struct sensors-chip-name)))
All of the above appears to be correct, so I proceed to load the library:
(define-foreign-library libsensors (:unix (:or "libsensors.so.4" "libsensors.so")) (t (:default "libsensors.so"))) (use-foreign-library libsensors)
No issues, so I define a c function from the c header file that parses a sensor chip name-string into a struct:
(defcfun ("sensors_parse_chip_name" cffi-sensors-parse-chip-name) :int "Parse a chip name to the internal representation. Return 0 on success, <0 on error." (orig-name :string) (res (:pointer (:struct sensors-chip-name))))
Which is based upon the following header function declaration:
int sensors_parse_chip_name(const char *orig_name, sensors_chip_name *res);
Then I implement a simple call to print out the parsed values (not including the bus):
(defun test-sensors-parse-chip-name (string) (with-foreign-object (name '(:struct sensors-chip-name)) (unless (= 0 (cffi-sensors-parse-chip-name string name)) (error "Failed to parse: ~A" string)) (with-foreign-slots ((prefix address path) name (:struct sensors-chip-name)) (format t "~%Prefix: '~A'" prefix) (format t "~%Address: '~A'" address) (format t "~%Path: '~A'" path))))
The above when executed gives the following output before the error:
CL-LMSENSORS> (test-sensors-parse-chip-name "atk0110-acpi-0")
Prefix: 'atk0110' Address: '0'
And once it tries to access the path variable, it gives the following error (sorry a lot rolls off the screen):
Unhandled memory fault at #xE71B7. [Condition of type SB-SYS:MEMORY-FAULT-ERROR]
Restarts: 0: [RETRY] Retry SLIME REPL evaluation request. 1: [*ABORT] Return to SLIME's top level. 2: [ABORT] abort thread (#<THREAD "repl-thread" RUNNING {1002207F93}>)
Backtrace: 0: (CFFI::FOREIGN-STRING-LENGTH #.(SB-SYS:INT-SAP #X000E71B7) :ENCODING :UTF-8 :OFFSET 0) Locals: #:.DEFAULTING-TEMP. = :UTF-8 #:.DEFAULTING-TEMP.#1 = 0 #:N-SUPPLIED-0 = 1 POINTER = #.(SB-SYS:INT-SAP #X000E71B7) 1: (FOREIGN-STRING-TO-LISP #.(SB-SYS:INT-SAP #X000E71B7) :OFFSET 0 :COUNT NIL :MAX-CHARS 4611686018427387900 :ENCODING :UTF-8$ Locals: #:.DEFAULTING-TEMP. = 0 #:.DEFAULTING-TEMP.#1 = NIL #:.DEFAULTING-TEMP.#2 = 4611686018427387900 #:.DEFAULTING-TEMP.#3 = :UTF-8 ENCODING = :UTF-8 #:N-SUPPLIED-0 = 1 POINTER = #.(SB-SYS:INT-SAP #X000E71B7) 2: ((:METHOD TRANSLATE-FROM-FOREIGN (T CFFI::FOREIGN-STRING-TYPE)) #.(SB-SYS:INT-SAP #X000E71B7) #<CFFI::FOREIGN-STRING-TYPE $ 3: (TEST-SENSORS-PARSE-CHIP-NAME "atk0110-acpi-0")
My guess is I'm not defining the (:struct sensors-chip-name) correctly, so that when sensors_parse_chip_name writes values into the struct, it somehow overlaps into the path string.
When looking at the source for sensors_parse_chip_name I've noticed that it does not set or access the path value at all. So I decided to write a small c program to test the native behavior:
elliott@desktop ~ $ cat test-sensors.c #include <stdio.h> #include <stdlib.h> #include "sensors/sensors.h"
int main( ) {
//sensors_init( NULL );
struct sensors_chip_name name;
int return_value = sensors_parse_chip_name( "atk0110-acpi-0", &name); if ( return_value != 0 ) { printf( "Failed to get a good return value." ); exit( return_value ); }
printf( "Printing the prefix: '%s'\n", name.prefix ); printf( "Address: '%i'\n", name.addr ); printf( "Printing path value: '%s'\n", name.path ); printf ("\n exiting..." );
exit( 0 ); } elliott@desktop ~ $ gcc test-sensors.c -l sensors -o test-sensors elliott@desktop ~ $ ./test-sensors Printing the prefix: 'atk0110' Address: '0' Printing path value: ''
exiting...
No problems there, so I assume that the issue is all my lack of cffi-fu training. Any pointers (yes, pun intended) would be appreciated... sorry for such a long read, but I hope it helps to have the background. At the very least it helped me to frame the issue in my mind by writing this.
Regards, Elliott
I think that answer is "you are not supposed to do that" because there is no documentation for the slots of sensors_chip_name. It could work by luck in your C test case if the uninitialized name slot points to a null byte.
__Martin
On Thu, 26 Apr 2018 20:16:43 -0700, Elliott Johnson said:
Hello,
If this is not the correct list to report a user error, I apologize and will happily redirect my question where ever suitable.
I've been using cffi to make a trivial library to libsensors https://github.com/groeck/lm-sensors and am getting an odd memory fault error when referencing a string value from a groveled struct. After hitting my head against a wall for a day and a half, I thought I'd ask in case it's a simple mistake.
Here's my information:
Running a 64 bit linux install. My local version of the libsensors library is as follows:
elliott@desktop ~ $ file /usr/lib64/libsensors.so.4.4.0 /usr/lib64/libsensors.so.4.4.0: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, stripped elliott@desktop ~ $ sensors --version sensors version 3.4.0+git_83cafd29f28d463573750d897014ec7143217ae5 with libsensors version 3.4.0+git_83cafd29f28d463573750d897014ec7143217ae5
I'm using a fairly recent version of sbcl built from source that (apparently) passed all of the tests after the build:
CL-USER> (lisp-implementation-version) "1.4.6.140-f8d5864d0"
My version of cffi appears to be the latest:
CL-USER> (slot-value (asdf:find-system :cffi) 'asdf:version) "0.19.0"
My system uses cffi-grovel and defines the following structs:
(cstruct sensors-bus-id "sensors_bus_id" (type "type" :type :short) (nr "nr" :type :short))
(cstruct sensors-chip-name "sensors_chip_name" (prefix "prefix" :type :string) (bus "bus" :type (:struct sensors-bus-id)) (address "addr" :type :int) (path "path" :type :string))
These are based upon the definitions in sensors/sensors.h: https://github.com/groeck/lm-sensors/blob/master/lib/sensors.h
After asdf loading the system, the following groveler definitions are created (had to dig these out of the cached build files):
(cffi:defcstruct (sensors-bus-id :size 4) (type :short :offset 0) (nr :short :offset 2)) (cl:defconstant size-of-sensors-bus-id (cffi:foreign-type-size '(:struct sensors-bus-id))) (cffi:defcstruct (sensors-chip-name :size 24) (prefix :string :offset 0) (bus (:struct sensors-bus-id) :offset 8) (address :int :offset 12) (path :string :offset 16)) (cl:defconstant size-of-sensors-chip-name (cffi:foreign-type-size '(:struct sensors-chip-name)))
All of the above appears to be correct, so I proceed to load the library:
(define-foreign-library libsensors (:unix (:or "libsensors.so.4" "libsensors.so")) (t (:default "libsensors.so"))) (use-foreign-library libsensors)
No issues, so I define a c function from the c header file that parses a sensor chip name-string into a struct:
(defcfun ("sensors_parse_chip_name" cffi-sensors-parse-chip-name) :int "Parse a chip name to the internal representation. Return 0 on success, <0 on error." (orig-name :string) (res (:pointer (:struct sensors-chip-name))))
Which is based upon the following header function declaration:
int sensors_parse_chip_name(const char *orig_name, sensors_chip_name *res);
Then I implement a simple call to print out the parsed values (not including the bus):
(defun test-sensors-parse-chip-name (string) (with-foreign-object (name '(:struct sensors-chip-name)) (unless (= 0 (cffi-sensors-parse-chip-name string name)) (error "Failed to parse: ~A" string)) (with-foreign-slots ((prefix address path) name (:struct sensors-chip-name)) (format t "~%Prefix: '~A'" prefix) (format t "~%Address: '~A'" address) (format t "~%Path: '~A'" path))))
The above when executed gives the following output before the error:
CL-LMSENSORS> (test-sensors-parse-chip-name "atk0110-acpi-0")
Prefix: 'atk0110' Address: '0'
And once it tries to access the path variable, it gives the following error (sorry a lot rolls off the screen):
Unhandled memory fault at #xE71B7. [Condition of type SB-SYS:MEMORY-FAULT-ERROR]
Restarts: 0: [RETRY] Retry SLIME REPL evaluation request. 1: [*ABORT] Return to SLIME's top level. 2: [ABORT] abort thread (#<THREAD "repl-thread" RUNNING {1002207F93}>)
Backtrace: 0: (CFFI::FOREIGN-STRING-LENGTH #.(SB-SYS:INT-SAP #X000E71B7) :ENCODING :UTF-8 :OFFSET 0) Locals: #:.DEFAULTING-TEMP. = :UTF-8 #:.DEFAULTING-TEMP.#1 = 0 #:N-SUPPLIED-0 = 1 POINTER = #.(SB-SYS:INT-SAP #X000E71B7) 1: (FOREIGN-STRING-TO-LISP #.(SB-SYS:INT-SAP #X000E71B7) :OFFSET 0 :COUNT NIL :MAX-CHARS 4611686018427387900 :ENCODING :UTF-8$ Locals: #:.DEFAULTING-TEMP. = 0 #:.DEFAULTING-TEMP.#1 = NIL #:.DEFAULTING-TEMP.#2 = 4611686018427387900 #:.DEFAULTING-TEMP.#3 = :UTF-8 ENCODING = :UTF-8 #:N-SUPPLIED-0 = 1 POINTER = #.(SB-SYS:INT-SAP #X000E71B7) 2: ((:METHOD TRANSLATE-FROM-FOREIGN (T CFFI::FOREIGN-STRING-TYPE)) #.(SB-SYS:INT-SAP #X000E71B7) #<CFFI::FOREIGN-STRING-TYPE $ 3: (TEST-SENSORS-PARSE-CHIP-NAME "atk0110-acpi-0")
My guess is I'm not defining the (:struct sensors-chip-name) correctly, so that when sensors_parse_chip_name writes values into the struct, it somehow overlaps into the path string.
When looking at the source for sensors_parse_chip_name I've noticed that it does not set or access the path value at all. So I decided to write a small c program to test the native behavior:
elliott@desktop ~ $ cat test-sensors.c #include <stdio.h> #include <stdlib.h> #include "sensors/sensors.h"
int main( ) {
//sensors_init( NULL );
struct sensors_chip_name name;
int return_value = sensors_parse_chip_name( "atk0110-acpi-0", &name); if ( return_value != 0 ) { printf( "Failed to get a good return value." ); exit( return_value ); }
printf( "Printing the prefix: '%s'\n", name.prefix ); printf( "Address: '%i'\n", name.addr ); printf( "Printing path value: '%s'\n", name.path ); printf ("\n exiting..." );
exit( 0 ); } elliott@desktop ~ $ gcc test-sensors.c -l sensors -o test-sensors elliott@desktop ~ $ ./test-sensors Printing the prefix: 'atk0110' Address: '0' Printing path value: ''
exiting...
No problems there, so I assume that the issue is all my lack of cffi-fu training. Any pointers (yes, pun intended) would be appreciated... sorry for such a long read, but I hope it helps to have the background. At the very least it helped me to frame the issue in my mind by writing this.
Regards, Elliott
Martin,
Thanks for the reply.
On 4/27/18 3:42 AM, Martin Simmons wrote:
I think that answer is "you are not supposed to do that" because there is no documentation for the slots of sensors_chip_name.
I'm not sure that I follow your point on the lack of "documentation for the slots of sensors_chip_name".
I followed the definitions in the c header file linked to below. Here is the header info:
typedef struct sensors_bus_id { short type; short nr; } sensors_bus_id;
/* A chip name is encoded in this structure */ typedef struct sensors_chip_name { char *prefix; sensors_bus_id bus; int addr; char *path; } sensors_chip_name;
So I hope I'm supposed to do this. I don't see any other way than to follow the API.
It could work by luck in your C test case if the uninitialized name slot points to a null byte.
__Martin
In the case of the cffi code, I use (with-foreign-object ...) to allocate the struct, so to test, I added a statement to my test function to initialize the path to an empty string before calling the parse function (which shouldn't touch the path slot):
(defun test-sensors-parse-chip-name (string) (with-foreign-object (name '(:struct sensors-chip-name)) (setf (foreign-slot-value name '(:struct sensors-chip-name) 'path) "") (unless (= 0 (cffi-sensors-parse-chip-name string name)) (error "Failed to parse: ~A" string)) (with-foreign-slots ((prefix bus address path) name (:struct sensors-chip-name)) (format t "~%Prefix: '~A'" prefix) (format t "~%Address: '~A'" address) (format t "~%Path: '~A'" path))))
When I execute this I get the following without error:
CL-LMSENSORS> (test-sensors-parse-chip-name "atk0110-acpi-0")
Prefix: 'atk0110' Address: '0' Path: '' NIL
So I'm guessing that cffi requires I do some setup on freshly allocated objects. I guess this makes sense, but isn't really communicated in the documentation. All part of being a stranger in a "land of C" that values the speed of malloc over the certainty of calloc.
Looking at FOREIGN-ALLOC, it seems that :INITIAL-ELEMENTS can be a provided argument, but that functionality is buried when calling WITH-FOREIGN-OBJECT[S]. The whole topic seems ripe for an :INITFORM argument to DEFSTRUCT that when provided assigns initial values to slots after allocation. I'm not sure if others would agree with such a feature, but it makes sense to me.
Martin, thanks again for your comments. They triggered the right set of challenges to my ever lurking assumptions.
Cheers, Elliott
On Thu, 26 Apr 2018 20:16:43 -0700, Elliott Johnson said:
Hello,
If this is not the correct list to report a user error, I apologize and will happily redirect my question where ever suitable.
I've been using cffi to make a trivial library to libsensors https://github.com/groeck/lm-sensors and am getting an odd memory fault error when referencing a string value from a groveled struct. After hitting my head against a wall for a day and a half, I thought I'd ask in case it's a simple mistake.
Here's my information:
Running a 64 bit linux install. My local version of the libsensors library is as follows:
elliott@desktop ~ $ file /usr/lib64/libsensors.so.4.4.0 /usr/lib64/libsensors.so.4.4.0: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, stripped elliott@desktop ~ $ sensors --version sensors version 3.4.0+git_83cafd29f28d463573750d897014ec7143217ae5 with libsensors version 3.4.0+git_83cafd29f28d463573750d897014ec7143217ae5
I'm using a fairly recent version of sbcl built from source that (apparently) passed all of the tests after the build:
CL-USER> (lisp-implementation-version) "1.4.6.140-f8d5864d0"
My version of cffi appears to be the latest:
CL-USER> (slot-value (asdf:find-system :cffi) 'asdf:version) "0.19.0"
My system uses cffi-grovel and defines the following structs:
(cstruct sensors-bus-id "sensors_bus_id" (type "type" :type :short) (nr "nr" :type :short))
(cstruct sensors-chip-name "sensors_chip_name" (prefix "prefix" :type :string) (bus "bus" :type (:struct sensors-bus-id)) (address "addr" :type :int) (path "path" :type :string))
These are based upon the definitions in sensors/sensors.h: https://github.com/groeck/lm-sensors/blob/master/lib/sensors.h
After asdf loading the system, the following groveler definitions are created (had to dig these out of the cached build files):
(cffi:defcstruct (sensors-bus-id :size 4) (type :short :offset 0) (nr :short :offset 2)) (cl:defconstant size-of-sensors-bus-id (cffi:foreign-type-size '(:struct sensors-bus-id))) (cffi:defcstruct (sensors-chip-name :size 24) (prefix :string :offset 0) (bus (:struct sensors-bus-id) :offset 8) (address :int :offset 12) (path :string :offset 16)) (cl:defconstant size-of-sensors-chip-name (cffi:foreign-type-size '(:struct sensors-chip-name)))
All of the above appears to be correct, so I proceed to load the library:
(define-foreign-library libsensors (:unix (:or "libsensors.so.4" "libsensors.so")) (t (:default "libsensors.so"))) (use-foreign-library libsensors)
No issues, so I define a c function from the c header file that parses a sensor chip name-string into a struct:
(defcfun ("sensors_parse_chip_name" cffi-sensors-parse-chip-name) :int "Parse a chip name to the internal representation. Return 0 on success, <0 on error." (orig-name :string) (res (:pointer (:struct sensors-chip-name))))
Which is based upon the following header function declaration:
int sensors_parse_chip_name(const char *orig_name, sensors_chip_name *res);
Then I implement a simple call to print out the parsed values (not including the bus):
(defun test-sensors-parse-chip-name (string) (with-foreign-object (name '(:struct sensors-chip-name)) (unless (= 0 (cffi-sensors-parse-chip-name string name)) (error "Failed to parse: ~A" string)) (with-foreign-slots ((prefix address path) name (:struct sensors-chip-name)) (format t "~%Prefix: '~A'" prefix) (format t "~%Address: '~A'" address) (format t "~%Path: '~A'" path))))
The above when executed gives the following output before the error:
CL-LMSENSORS> (test-sensors-parse-chip-name "atk0110-acpi-0")
Prefix: 'atk0110' Address: '0'
And once it tries to access the path variable, it gives the following error (sorry a lot rolls off the screen):
Unhandled memory fault at #xE71B7. [Condition of type SB-SYS:MEMORY-FAULT-ERROR]
Restarts: 0: [RETRY] Retry SLIME REPL evaluation request. 1: [*ABORT] Return to SLIME's top level. 2: [ABORT] abort thread (#<THREAD "repl-thread" RUNNING {1002207F93}>)
Backtrace: 0: (CFFI::FOREIGN-STRING-LENGTH #.(SB-SYS:INT-SAP #X000E71B7) :ENCODING :UTF-8 :OFFSET 0) Locals: #:.DEFAULTING-TEMP. = :UTF-8 #:.DEFAULTING-TEMP.#1 = 0 #:N-SUPPLIED-0 = 1 POINTER = #.(SB-SYS:INT-SAP #X000E71B7) 1: (FOREIGN-STRING-TO-LISP #.(SB-SYS:INT-SAP #X000E71B7) :OFFSET 0 :COUNT NIL :MAX-CHARS 4611686018427387900 :ENCODING :UTF-8$ Locals: #:.DEFAULTING-TEMP. = 0 #:.DEFAULTING-TEMP.#1 = NIL #:.DEFAULTING-TEMP.#2 = 4611686018427387900 #:.DEFAULTING-TEMP.#3 = :UTF-8 ENCODING = :UTF-8 #:N-SUPPLIED-0 = 1 POINTER = #.(SB-SYS:INT-SAP #X000E71B7) 2: ((:METHOD TRANSLATE-FROM-FOREIGN (T CFFI::FOREIGN-STRING-TYPE)) #.(SB-SYS:INT-SAP #X000E71B7) #<CFFI::FOREIGN-STRING-TYPE $ 3: (TEST-SENSORS-PARSE-CHIP-NAME "atk0110-acpi-0")
My guess is I'm not defining the (:struct sensors-chip-name) correctly, so that when sensors_parse_chip_name writes values into the struct, it somehow overlaps into the path string.
When looking at the source for sensors_parse_chip_name I've noticed that it does not set or access the path value at all. So I decided to write a small c program to test the native behavior:
elliott@desktop ~ $ cat test-sensors.c #include <stdio.h> #include <stdlib.h> #include "sensors/sensors.h"
int main( ) {
//sensors_init( NULL );
struct sensors_chip_name name;
int return_value = sensors_parse_chip_name( "atk0110-acpi-0", &name); if ( return_value != 0 ) { printf( "Failed to get a good return value." ); exit( return_value ); }
printf( "Printing the prefix: '%s'\n", name.prefix ); printf( "Address: '%i'\n", name.addr ); printf( "Printing path value: '%s'\n", name.path ); printf ("\n exiting..." );
exit( 0 ); } elliott@desktop ~ $ gcc test-sensors.c -l sensors -o test-sensors elliott@desktop ~ $ ./test-sensors Printing the prefix: 'atk0110' Address: '0' Printing path value: ''
exiting...
No problems there, so I assume that the issue is all my lack of cffi-fu training. Any pointers (yes, pun intended) would be appreciated... sorry for such a long read, but I hope it helps to have the background. At the very least it helped me to frame the issue in my mind by writing this.
Regards, Elliott
On Fri, 27 Apr 2018 16:48:57 -0700, Elliott Johnson said:
On 4/27/18 3:42 AM, Martin Simmons wrote:
I think that answer is "you are not supposed to do that" because there is no documentation for the slots of sensors_chip_name.
I'm not sure that I follow your point on the lack of "documentation for the slots of sensors_chip_name".
I followed the definitions in the c header file linked to below. Here is the header info:
I meant the documentation in https://github.com/groeck/lm-sensors/blob/master/lib/libsensors.3 (or man libsensors).
C header files often contain internal details that you are not supposed to use.
In the case of the cffi code, I use (with-foreign-object ...) to allocate the struct, so to test, I added a statement to my test function to initialize the path to an empty string before calling the parse function (which shouldn't touch the path slot):
(defun test-sensors-parse-chip-name (string) (with-foreign-object (name '(:struct sensors-chip-name)) (setf (foreign-slot-value name '(:struct sensors-chip-name) 'path) "") (unless (= 0 (cffi-sensors-parse-chip-name string name)) (error "Failed to parse: ~A" string)) (with-foreign-slots ((prefix bus address path) name (:struct sensors-chip-name)) (format t "~%Prefix: '~A'" prefix) (format t "~%Address: '~A'" address) (format t "~%Path: '~A'" path))))
When I execute this I get the following without error:
Yes, that will "work" but I still have doubts that this is the correct way to use sensors_parse_chip_name. I think it is supposed to used as the "match" argument to sensors_get_detected_chips.
It looks like the sensors_chip_name struct is used within the implementation of the library itself, where is does make use of the path slot.
__Martin
On 4/30/18 5:39 AM, Martin Simmons wrote:
I meant the documentation in https://github.com/groeck/lm-sensors/blob/master/lib/libsensors.3 (or man libsensors).
C header files often contain internal details that you are not supposed to use.
You make a very good point that those structs are not part of the public API. I've removed the slots from the groveler's struct definitions and the exported functions accept them and work well. I'm glad I asked, since I think I'll approach building future ffi projects differently.
Thanks, Elliott