We will require a couple of files for this recipe. Let us start with the C++ implementation and interface. We will place these in a subdirectory called account/implementation. The implementation file (cpp_implementation.cpp) is similar to that in previous recipes but contains additional assert statements, since we will keep the state of the object in an opaque handle and we will have to make sure that the object is created before we try to access it:
#include "cpp_implementation.hpp"
#include <cassert>
Account::Account() {
balance = 0.0;
is_initialized = true;
}
Account::~Account() {
assert(is_initialized);
is_initialized = false;
}
void Account::deposit(const double amount) {
assert(is_initialized);
balance += amount;
}
void Account::withdraw(const double amount) {
assert(is_initialized);
balance -= amount;
}
double Account::get_balance() const {
assert(is_initialized);
return balance;
}
The interface file (cpp_implementation.hpp) contains the following:
#pragma once
class Account {
public:
Account();
~Account();
void deposit(const double amount);
void withdraw(const double amount);
double get_balance() const;
private:
double balance;
bool is_initialized;
};
In addition, we isolate a C—C++ interface (c_cpp_interface.cpp). This will be the interface we will try to hook into with Python CFFI:
#include "account.h"
#include "cpp_implementation.hpp"
#define AS_TYPE(Type, Obj) reinterpret_cast<Type *>(Obj)
#define AS_CTYPE(Type, Obj) reinterpret_cast<const Type *>(Obj)
account_context_t *account_new() {
return AS_TYPE(account_context_t, new Account());
}
void account_free(account_context_t *context) { delete AS_TYPE(Account, context); }
void account_deposit(account_context_t *context, const double amount) {
return AS_TYPE(Account, context)->deposit(amount);
}
void account_withdraw(account_context_t *context, const double amount) {
return AS_TYPE(Account, context)->withdraw(amount);
}
double account_get_balance(const account_context_t *context) {
return AS_CTYPE(Account, context)->get_balance();
}
One directory up, under account, we describe the C interface (account.h):
/* CFFI would issue warning with pragma once */
#ifndef ACCOUNT_H_INCLUDED
#define ACCOUNT_H_INCLUDED
#ifndef ACCOUNT_API
#include "account_export.h"
#define ACCOUNT_API ACCOUNT_EXPORT
#endif
#ifdef __cplusplus
extern "C" {
#endif
struct account_context;
typedef struct account_context account_context_t;
ACCOUNT_API
account_context_t *account_new();
ACCOUNT_API
void account_free(account_context_t *context);
ACCOUNT_API
void account_deposit(account_context_t *context, const double amount);
ACCOUNT_API
void account_withdraw(account_context_t *context, const double amount);
ACCOUNT_API
double account_get_balance(const account_context_t *context);
#ifdef __cplusplus
}
#endif
#endif /* ACCOUNT_H_INCLUDED */
We also describe the Python interface, which we will comment on below (__init__.py):
from subprocess import check_output
from cffi import FFI
import os
import sys
from configparser import ConfigParser
from pathlib import Path
def get_lib_handle(definitions, header_file, library_file):
ffi = FFI()
command = ['cc', '-E'] + definitions + [header_file]
interface = check_output(command).decode('utf-8')
# remove possible \r characters on windows which
# would confuse cdef
_interface = [l.strip('\r') for l in interface.split('\n')]
ffi.cdef('\n'.join(_interface))
lib = ffi.dlopen(library_file)
return lib
# this interface requires the header file and library file
# and these can be either provided by interface_file_names.cfg
# in the same path as this file
# or if this is not found then using environment variables
_this_path = Path(os.path.dirname(os.path.realpath(__file__)))
_cfg_file = _this_path / 'interface_file_names.cfg'
if _cfg_file.exists():
config = ConfigParser()
config.read(_cfg_file)
header_file_name = config.get('configuration', 'header_file_name')
_header_file = _this_path / 'include' / header_file_name
_header_file = str(_header_file)
library_file_name = config.get('configuration', 'library_file_name')
_library_file = _this_path / 'lib' / library_file_name
_library_file = str(_library_file)
else:
_header_file = os.getenv('ACCOUNT_HEADER_FILE')
assert _header_file is not None
_library_file = os.getenv('ACCOUNT_LIBRARY_FILE')
assert _library_file is not None
_lib = get_lib_handle(definitions=['-DACCOUNT_API=', '-DACCOUNT_NOINCLUDE'],
header_file=_header_file,
library_file=_library_file)
# we change names to obtain a more pythonic API
new = _lib.account_new
free = _lib.account_free
deposit = _lib.account_deposit
withdraw = _lib.account_withdraw
get_balance = _lib.account_get_balance
__all__ = [
'__version__',
'new',
'free',
'deposit',
'withdraw',
'get_balance',
]
This is a handful of files, but, as we will see, most of this interface work is generic and reusable and the actual interface is rather thin. To summarize, this is the layout of our project:
.
├── account
│ ├── account.h
│ ├── CMakeLists.txt
│ ├── implementation
│ │ ├── c_cpp_interface.cpp
│ │ ├── cpp_implementation.cpp
│ │ └── cpp_implementation.hpp
│ ├── __init__.py
│ └── test.py
└── CMakeLists.txt