Wrapping TensorFlow C API with WrapC

by javier (modified: 2020 Mar 12)

Wrapping TensorFlow C API with WrapC

In this tutorial, we are going to see how we can wrap a C API with WrapC.

WrapC is an Eiffel wrapper generator for C libraries. It is used to create libraries that bridge the gap between Eiffel and C. Originally known as EWG, WrapC is an updated version using the latest Eiffel version.

For more see: https://github.com/eiffel-wrap-c/WrapC

Installation

First, we need to install WrapC and TensorFlow for C

Installing WrapC

To install WrapC:

  • clone or download it from https://github.com/eiffel-wrap-c/WrapC
  • Compile the binary:
    • Assume $WRAP_C is the directory where you unpacked the WrapC code.
    • Open with EiffelStudio the project located at $WRAP_C/src/ewg/system.ecf and finalize it. Then copy the binary from the EIFGENs/.../F_code into $WRAP_C/bin, you can also put it under your PATH variable.

To learn more check WrapC Developer Guide

Check wrap_c binary

$>wrap_c --version wrap_c version 1.0.0

Installing TensorFlow for C

TensorFlow provides a C API that can be used to generate bindings for other languages. This section is based on TensorFlow installation guide

Linux

For this tutorial, we use the Linux CPU binarieshttps://storage.googleapis.com/tensorflow/libtensorflow/libtensorflow-cpu-linux-x86_64-1.14.0.tar.gz

Extract the code sudo tar -C /usr/local -xzf libtensorflow-cpu-linux-x86_64-1.14.0.tar.gzLinker

On Linux/macOS, if you extract the TensorFlow C library to a system directory, such as /usr/local, configure the linker with ldconfig:

sudo ldconfig

Example program

With the TensorFlow C library installed, create an example program with the following source code (hello_tf.c):

#include <stdio.h> #include <tensorflow/c/c_api.h> int main() { printf("Hello from TensorFlow C library version %s\n", TF_Version()); return 0; }

Compile

Compile the example program to create an executable, then run:

gcc hello_tf.c -ltensorflow -o hello_tf ./hello_tf

Windows

For this tutorial, we use the Windows CPU binarieshttps://storage.googleapis.com/tensorflow/libtensorflow/libtensorflow-cpu-windows-x86_64-1.14.0.zip.

For this tutorial, we are using Microsft C/C++ compiler.

Extract the content of the zip file. Then we need to update the file c_api.h replace the line #include "tensorflow/c/tf_attrtype.h" with #include "tf_attrtype.h" Finally download the file https://github.com/tensorflow/tensorflow/blob/r1.14/tensorflow/c/tf_attrtype.h since it's not included in the zip file.

Example program

With the TensorFlow C library installed, create an example program with the following source code (hello_tf.c):

#include <stdio.h> #include "c_api.h" int main() { printf("Hello from TensorFlow C library version %s\n", TF_Version()); return 0; }Here we are assuming our source code hello_tc.c is at the same directory where the TensorFlow headers are located. Be sure to have in the path or in the same directory TensorFlow lib and dll files.

Compile

cl hello_tf.c /link tensorflow.lib

The command outputs: Hello from TensorFlow C library version number.For more details check the installation guide

Create a new project to wrap TensorFlow C API

In the previous step, you have downloaded WrapC code. Got to the directory $WRAP_C/examples you will see something like this

examples callback simple template To generate a new Eiffel wrapper, the simplest way is to start from the template wrapping example located at $WRAP_C/examples/template

Directory Structure

template |___ library config.xml -- configuration file to customize the way EWG generates the wrapper. build.eant -- build script. library.ecf -- library configuration file. scripts -- optional pre or post-processing scripts

Wrapping Development Process

Step 1: Copy and Rename the template project

Copy the template project (located at $WRAP_C/examples/template to a new location for example $NEW_PATH/test_wrapc/template) and rename it: for example as: wrap_tensorflow so your new project now will be $NEW_PATH/test_wrapc/wrapc_tensorflow`

Now go to $NEW_PATH/test_wrapc/wrapc_tensorflow/library directory

  • Rename and update the library.ecf as libtensorflow.ecf
    • update the libtensorflow.ecf file.
      • replace all the occurrences of template_library by libtensorflow.
      • update externals include location <external_include location="$HEADER_TEMPLATE_PATH"> with the directory path to the C header/s

Create a new directory C under $NEW_PATH/test_wrapc/wrapc_tensorflow/library, now you will see something like this

Note: is not required to create the C directory you can use the path to where you have the TensorFlow libs and headers.

template |___ library C -- directory to include headers and libs of the target C library. config.xml -- configuration file to customize the way EWG generates the wrapper. build.eant -- build script. library.ecf -- library configuration file. scripts -- optional pre or post-processing scripts Copy the TensorFlow lib and include folders into the C folder, that you have already created.When your Eiffel executable is running, Eiffel TensorFlow library needs tensorflow.dll. Ne sure the DLL file can be found in your environment PATH or they are in the same folder of your executable.

At this point, our libtensorflow.ecf will look like as follow

<?xml version="1.0" encoding="ISO-8859-1"?> <system xmlns="http://www.eiffel.com/developers/xml/configuration-1-21-0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.eiffel.com/developers/xml/configuration-1-21-0 http://www.eiffel.com/developers/xml/configuration-1-21-0.xsd" name="libtensorflow" uuid="E7B07AD1-10D6-4D4E-A36D-5A7E7C0AA970" library_target="libtensorflow"> <target name="libtensorflow"> <root all_classes="true"/> <option warning="warning" full_class_checking="false" is_attached_by_default="true" manifest_array_type="mismatch_warning"> <assertions precondition="true"/> </option> <setting name="console_application" value="true"/> <setting name="inlining_size" value="0"/> <setting name="msil_classes_per_module" value="5"/> <setting name="dead_code_removal" value="feature"/> <external_include location="$ECF_CONFIG_PATH\generated_wrapper\c\include"> <condition> <platform value="windows"/> </condition> </external_include> <external_include location="$ECF_CONFIG_PATH\C\include\tensorflow\c"> <condition> <platform value="windows"/> </condition> </external_include> <external_include location="/usr/local/include/tensorflow/c"> <condition> <platform excluded_value="windows"/> </condition> </external_include> <external_include location="$ECF_CONFIG_PATH/generated_wrapper/c/include"> <condition> <platform excluded_value="windows"/> </condition> </external_include> <external_object location="$ECF_CONFIG_PATH/C/lib/tensorflow.lib"> <condition> <platform value="windows"/> </condition> </external_object> <external_object location="$ECF_CONFIG_PATH/C/spec/$ISE_C_COMPILER/$ISE_PLATFORM/lib/eif_libtensorflow.lib"> <condition> <platform value="windows"/> </condition> </external_object> <external_object location="$ECF_CONFIG_PATH/C/spec/$(ISE_PLATFORM)/lib/eif_libtensorflow.a"> <condition> <platform excluded_value="windows"/> </condition> </external_object> <external_linker_flag value="-ltensorflow "> <condition> <platform excluded_value="windows"/> </condition> </external_linker_flag> <library name="base" location="$ISE_LIBRARY\library\base\base.ecf"/> <cluster name="src" location=".\" recursive="true"> <file_rule> <exclude>/EIFGENs$</exclude> <exclude>/c$</exclude> <exclude>/include$</exclude> </file_rule> </cluster> </target> </system>

Step 2: Configure config.xml

To customize the way WrapC generate wrappers, we will update the configuration file config.xml located at $NEW_PATH/test_wrapc/wrapc_tensorflow/library. Initially, we will start only including the function to print the tensor flow version. This is the file we will update to wrap Structs, Unions, Functions, Callbacks, and Macros.

<?xml version="1.0"?> <ewg_config name="libtensorflow"> <rule_list> <!-- Ignore everything that starts with an underscore. --> <rule> <match> <identifier name="_.*"/> </match> <wrapper type="none"> </wrapper> </rule> <!-- Include TF function TF_Version--> <rule> <match> <identifier name="TF_Version"/> </match> <wrapper type="default"> </wrapper> </rule> <!-- And the rest should be ignored --> <rule> <match> </match> <wrapper type="none"> </wrapper> </rule> </rule_list> </ewg_config>At this point our configuration file includes only the library function TF_Version and WrapC will generate a class C_API_FUNCTIONS_API with the corresponding Eiffel feature. The name of the class is C_API_FUNCTIONS_API because the function declaration comes from a C header file named c_api.h. Later we will see we can group all the TensorFlow functions defined in c_api.h using a uniform class name.

So now we are ready to use the wrap_c, we can use geant tool or just run the tool directly. For this tutorial, we will use the wrap_c tool directly, check the sour code to see the geant file build.eant , to learn more about geant check the following link

We will execute the following command:

Linux

wrap_c --verbose --c_compile_options=-I/usr/local/include/tensorflow/c --output-dir=./generated_wrapper --full-header=/usr/local/include/tensorflow/c/c_api.h --config=config.xmlOn Linux we are using the default location for TensorFlow installation, change if you use a different path.

Windows

wrap_c --verbose --output-dir=%cd%/generated_wrapper --full-header=%cd%/C/include/tensorflow/c/c_api.h --config=%cd%/config.xmlOn Windows the TensorFlow C headers and lib (.lib and dll) are located at $NEW_PATH/test_wrapc/wrapc_tensorflow/library/C if you create it as describe previously, in other case change it, if you use a different path.

WrapC execution

wrap_c --verbose --c_compile_options=-I/usr/local/include/tensorflow/c --output-dir=./generated_wrapper --full-header=/usr/local/include/tensorflow/c/c_api.h --config=config.xml [100%] phase 1: parsing parsed successfuly found: . 424 types . 178 declarations [100%] phase 2: resolving aliases for anonymous types [100%] phase 3: wrapping declarations [100%] phase 3: selecting types to wrap [100%] phase 3: wrapping types found: . 0 enum wrappers . 0 struct wrappers . 0 union wrappers . 1 function wrappers . 0 callback wrappers [100%] phase 4: generating function wrappers phase 5: generating Makefiles and WrapC header

Linux

Finally, for this library, we need to update the generated MakeFile.SH file located at /generated_wrapper/c/src, to fix the CFLAGS line, since the MakeFile generation code takes the name of the config file and tries to include the headers using pkg-config. In the future, WrapC code should be improved to handle this issue. As a workaround, we can use the post_processing facilities to automate this process. Check understanding wrapc to learn more about the command-line options.

For this tutorial, we will first update the file manually, and then we will use the post_processing command-line option, to automate the process.

Linux: Makefile.SH update

CFLAGS = $optimize $ccflags $large -I$rt_include -I../../../generated_wrapper/c/include -I../include `pkg-config --cflags libtensorflow` -I../../../C/include With

CFLAGS = $optimize $ccflags $large -I$rt_include -I/usr/local/include/tensorflow/c -I../../../generated_wrapper/c/include -I../include -I../../../C/includeSince we will need to run this command multiple times during the wrapping process we save a copy of this file at the library root ($NEW_PATH/test_wrapc/wrapc_tensorflow/library) and we will create a script to be called at the post_processing step.

post_script.sh #!/bin/sh # Post processing script #copy Makefile cp Makefile.SH ./generated_wrapper/c/srcBe sure to execute chmod +x post_script.sh to set executable permissions.

Linux: WrapC command line update

wrap_c --verbose --c_compile_options=-I/usr/local/include/tensorflow/c --script_post_process=post_script.sh --output-dir=./generated_wrapper --full-header=/usr/local/include/tensorflow/c/c_api.h --config=config.xml So now, we are able to execute the wrapping process without the need to manually fix the Makefile.SH.

Compile C lib

Finally, to compile the C lib, go to /generated_wrapper/c/src and run finish_freezing --library

It will copy the C lib eif_libtensorflow.a under the library root $ECF_CONFIG_PATH/C/spec/$(ISE_PLATFORM)/lib/eif_libtensorflow.a

Automate the process

Before we start, all the scripts that we will need will be created at $NEW_PATH/test_wrapc/wrapc_tensorflow/library.As you can see this process will be repeated multiple times during the development, so we will create a new pre-processing script pre_scriptto delete the generated code. Update the script post_script on Linux and create a new one on Windows, so it can also compile the C lib and finally, we will add a new script generator to call the wrap_c tool.

First, let's create the pre-processing scripts for Linux and Windows

Linux: pre_script.sh#!/bin/sh # Pre processing script #Delete generated code echo Removing generated code. rm -r generated_wrapper cd C/ rm -r specWindows: pre_script.bat@echo on title pre_process script echo Removing generated code. set current_dir = %~dp0 rd /s /q generated_wrapper cd %current_dir%C/ rd /s /q spec cd ..Linux: Update thepost_script.sh to compile the C lib

#!/bin/sh # Post processing script #copy Makefile echo Copy MakeFile cp Makefile.SH ./generated_wrapper/c/src cd generated_wrapper/c/src/ finish_freezing -libraryWindows: Createpost_script.bat to compile the C lib

@echo on title post_process script cd generated_wrapper/c/src/ finish_freezing -library

Finally, we add the generator script to automate the process

Linux: generator.sh#!/bin/sh #Script to automate WrapC development process. wrap_c --verbose --c_compile_options=-I/usr/local/include/tensorflow/c --script_pre_process=pre_script.sh --script_post_process=post_script.sh --output-dir=./generated_wrapper --full-header=/usr/local/include/tensorflow/c/c_api.h --config=config.xml

Windows: generator.bat

@echo on title Script to automate WrapC development process. wrap_c --verbose --script_pre_process=pre_script.bat --script_post_process=post_script.bat --output-dir=%cd%/generated_wrapper --full-header=%cd%/C/include/tensorflow/c/c_api.h --config=%cd%/config.xmOn Linux be sure to execute chmod +x to set executable permissions to the new scripts. Now we only need to execute the script generator, every time we change our wrap_c configuratin file config.xml.

Example using our TensorFlow Library

Create a new project and include the TensorFlow library that we just wrapped in the previous section, the first example will show the TensorFlow version, as we saw previously with the C example.

First, update the APPLICATION class as following

class APPLICATION create make feature -- Initialization make do print ("%NTensorFlow Version: " + (create {C_STRING}.make_by_pointer ({C_API_FUNCTIONS_API}.tf_version)).string ) end endNow execute it, it will display the TensorFlow library version in the command line

TensorFlow Version: 1.14.0In this case, as you can see, our wrapper is too low level and we need to create a STRING from a pointer.

class C_API_FUNCTIONS_API feature -- Access tf_version: POINTER external "C inline use <c_api.h>" alias "[ return TF_Version (); ]" end end

Manual Wrapping

During the wrapping process we will need to do some manual wrapping to create high-level classes to access the low-level wrapper, or just to fix or wrap things that WrapC can't do.

In our previous example when we want to print the TensorFlow version we need to create a STRING from a C pointer, so instead of doing this all the time, we can create a new class and create a reusable feature.

We will create a new class TENSORFLOW_API under the manual_wrapper cluster as you can see in the following image.

To avoid repeating the following piece of code, this is a very simple case, but the idea is to create reusable code and hide low-level details.

my_string := create {C_STRING}.make_by_pointer ({C_API_FUNCTIONS_API}.tf_version)).string Here we create a manual class TENSORFLOW_API to have high-level features using the low-level wrapper class C_API_FUNCTIONS_API. In this particular example, I decided to use inheritance.

class TENSORFLOW_API inherit C_API_FUNCTIONS_API feature -- Access version: STRING -- Tensorflow version do if tf_version /= default_pointer then Result := (create {C_STRING}.make_by_pointer (tf_version)).string else Result := "Unkown" end end endNow in our example, we can use our new class as follow

class APPLICATION inherit TENSORFLOW_API create make feature -- Initialization make do print ("%NTensorFlow Version: " + version) end end

Updating the configuration file to wrap TensorFlow

We will start updating our configuration, let's start with functions

TensforFlow functions

We will add the following rule to groups all TensorFlow functions in a uniform class name TENSORFLOW_FUNCTIONS

<!-- Include all TF functions in a uniform class--> <rule> <match> <identifier name="TF_.*"/> <type name="function"/> </match> <wrapper type="default"> <class_name name="TENSORFLOW_FUNCTIONS"/> </wrapper> </rule>

  • type: Using type in the match clause to constrains the wrapping to functions.
  • class_name: Using class_name in the wrapper clause to group all functions in a uniform class. This option is only valid for functions and macros.

Adding this rule will also wrap dependent types structs and enums.

In this case, it is not needed to add anything else to wrap structs and enums, but if needed, add a rule to wrap structs or enums types filtered by a set of elements you want to wrap identifier for that given type.

TensforFlow callbacks

Here we will add a rule to wrap C callbacks, the TensorFlow C API defines a few callbacks, and all of them are anonymous.

TF_CAPI_EXPORT extern TF_Tensor* TF_NewTensor( TF_DataType, const int64_t* dims, int num_dims, void* data, size_t len, void (*deallocator)(void* data, size_t len, void* arg), void* deallocator_arg);

Initially, we should try with the following rules

<!-- Include all TF callbacks --> <rule> <match> <identifier name=".*"/> <type name="callaback"/> </match> <wrapper type="default"> </wrapper> </rule>

If this does not work, we can use a generic way

<!-- Include all TF callbacks --> <rule> <match> <type name="any"/> </match> <wrapper type="default"> </wrapper> </rule>

Our final configuration file will look like this

<?xml version="1.0"?> <ewg_config name="libtensorflow"> <rule_list> <!-- Ignore everything that starts with an underscore. --> <rule> <match> <identifier name="_.*"/> </match> <wrapper type="none"> </wrapper> </rule> <!-- Include all TF functions in a uniform class--> <rule> <match> <identifier name="TF_.*"/> <type name="function"/> </match> <wrapper type="default"> <class_name name="TENSORFLOW_FUNCTIONS"/> </wrapper> </rule> <!-- Include all TF callbacks --> <rule> <match> <type name="any"/> </match> <wrapper type="default"> </wrapper> </rule> <!-- And the rest should be ignored --> <rule> <match> </match> <wrapper type="none"> </wrapper> </rule> </rule_list> </ewg_config>