Add Babeltrace 2 Python bindings
This patch adds new Babeltrace 2 Python bindings to the Babeltrace
project.
Those bindings are compatible with Python 3 (only).
The new bindings still make use of SWIG to simplify the Python-to-native
and native-to-Python calls.
The new bindings, from Python's point of view, are available in the new
`bt2` package. This package imports (__init__.py does) everything that
is public from its modules, so that every public name is available as
bt2.something. This is considered more Pythonic than asking the user to
import specific modules from a given package.
The goal with this, to keep the "old" `babeltrace` package working, is
to make the `babeltrace` package a simple Python-only wrapper of the bt2
package (which offers much more, as you will discover in this commit
message).
Summary of features:
* All the current Babeltrace 2 APIs are wrapped:
* Clock class
* Component, component class, notification iterator, and notification
* CTF writer, CTF writer clock, CTF writer stream
* Event and event classe
* Packet, stream, and stream class
* Fields and field types
* Plugin
* Trace
* Values
* Automatic BT reference count handling for the user (just like the
`babeltrace` package does).
* Type checking of the arguments of each method/function (before it gets
to SWIG, where the exception is not as obvious).
* Package exceptions:
* Error
* CreationError
* FrozenError
* UnsupportedFeature
* TryAgain
* Stop
* IncompleteUserClassError
* Full support of user component classes.
* Package is as Pythonic as possible, with extensive use of collection
ABCs, number and other protocols, iterators, properties, inheritance
for the user, and exceptions.
* Easy to extend if we ever add new BT objects or APIs.
* The bindings only use the public Babeltrace 2 C API; they could be
built outside the Babeltrace repository without altering the code.
Build system
============
Makefile.am does pretty much the same job as previously, although it is
organized so that it's easy to add Python modules and partial SWIG
interfaces. All the rules and commands are built from two simple lists.
SWIG
====
I created one SWIG interface file (.i) for each Babeltrace API. All the
native_bt*.i files are included at the end of native_bt.i. This is the
only input for SWIG.
native_bt.i does more than including partial interface files. It adds
rules to remove the bt_ and BT_ prefixes of all the wrapped functions
and enumeration items.
native_bt.i also adds a few custom typemaps to convert special arguments
back and forth between Python and the C API. For example,
`const char **BTOUTSTR` is a typemap to append a Python string (Unicode
object) to the current SWIG result tuple when the argument is named
as such, as in:
int bt_ctf_field_type_enumeration_get_mapping_signed(
struct bt_ctf_field_type *enum_field_type, int index,
const char **BTOUTSTR, int64_t *OUTPUT, int64_t *OUTPUT);
Note that, in example above, OUTPUT is a typemap provided by SWIG for
very simple types.
Another typemap is BTUUID to accept and return Babeltrace UUIDs as
Python `bytes` objects:
BTUUID bt_ctf_clock_class_get_uuid(struct bt_ctf_clock_class *clock_class);
int bt_ctf_clock_class_set_uuid(struct bt_ctf_clock_class *clock_class,
BTUUID uuid);
Modules
=======
I'll now go into the details of each module of the bt2 package, in a
relevant order.
Most of the objects described below are comparable. Their compare
function usually start with a simple address comparison, and then falls
back to a rich comparison (sometimes implemented in Python if the
equivalent C function is missing).
utils
-----
Small utility functions: type checking, automatic exception raising,
power of two, etc.
object
------
bt_object API and reference counting.
Base class for a wrapped BT object (with reference counting), not
meant to be instantiated by the user.
Provides:
* self._ptr: SWIG pointer (native BT object), for the functions of
the bt2 package (private).
* __init__(): called by subclass to wrap a SWIG pointer.
* addr(): public property which returns the address (integer, not the
SWIG pointer object) of the object, mostly for debug purposes (for the
user) and for a user to know if two bt2 objects actually wrap the
same BT native object.
* _create_from_ptr(): class method to wrap a given SWIG pointer as an
objet of a given class:
event_class = EventClass._create_from_ptr(ptr)
* __repr__(): Shows the type of the object and its native address.
* __del__(): Puts its BT object reference (calls bt_put()).
object.py also contains _Freezable, a mixin for publicly freezable
objects.
values
------
bt_value API.
The classes in bt2.values are full Python wrappers of the native
bt_value types.
Features:
* BoolValue acts just like Python's bool.
* IntegerValue acts just like Python's int.
* FloatValue acts just like Python's float.
* StringValue acts just like Python's str.
* ArrayValue acts just like Python's list.
* MapValue acts just like Python's dict.
* bt_value_null is the equivalent of None.
In other words:
* All types are comparable and copyable (copy/deep copy).
* All the needed operators are implemented to make the value objects
act like Python native objects.
* Number classes inherit ABCs in the numbers module.
There's also a bt2.create_value() function which returns a bt2.values
object from any value (bt2.values or native Python object).
I decided to wrap actual bt_value objects instead of always converting
from native Python objects to them and vice versa, because they can
still be shared in this case. Otherwise the conversion would remove this
sharing feature which is implicit with BT reference counting.
A few examples:
my_int = bt2.IntegerValue(23)
my_int += 283
print(my_int)
some_flt = bt2.FloatValue(45.3)
print(my_int * some_flt)
s = bt2.create_value('hello there')
print(s[3:9])
my_map = bt2.create_value({'ho': 23, 'meow': (4, 5, False, None)})
print(my_map)
print(my_map['meow'][1] >= 5)
print(my_map.addr)
for k, v in my_map.items():
print('{}: {}'.format(k, v))
field_types
-----------
bt_ctf_field_type API.
This looks pretty much like the original field type objects of
the `babeltrace.writer` module, except for the following features:
* `Declaration` suffix is replaced with `FieldType` suffix to match
the C API convention.
* Copy, deep copy, and comparison support.
* You can pass all the properties of an object at construction time:
int_ft = bt2.IntegerFieldType(size=23, align=16, is_signed=True,
base=8, mapped_clock_class=cc)
* Enumeration field type honors the sequence protocol:
for mapping in enum_ft:
print(mapping.name, mapping.lower, mapping.upper)
* Enumeration field type mapping iterator support, e.g.:
for mapping in enum_ft.mappings_by_name('APPLE'):
print(mapping.name, mapping.lower, mapping.upper)
It's easy to add the mappings of another enumeration field type:
enum_ft += other_enum_ft
* EnumerationFieldType inherits IntegerFieldType so that you can do:
enum_ft = bt2.EnumerationFieldType(size=23, align=16,
is_signed=True,
base=bt2.Base.HEXADECIMAL,
byte_order=bt2.ByteOrder.BIG_ENDIAN)
print(enum_ft.size)
enum_ft.is_signed = False
instead of getting the underlying integer field type object manually.
* Structure and variant field types honor the mapping protocol:
for name, ft in struct_ft:
print(name, ft)
* You can set the `min_alignment` property of a structure field type
(but you cannot get it), and you can get its `alignment` property
(but you cannot set it). Those names represent exactly what they
mean (less ambiguous than the C API equivalent IMO).
* You can instantiate a field object from a field type object by
"calling" the field type object. This is closer to the concept of
a class in the Python world:
my_field = int_ft()
fields
------
bt_ctf_field API.
A bt2._Field is the result of instantiating a field type object. The
type (and all its subclasses) starts with an underscore because you
cannot instatiate them directly (possibly with an initial value):
int_field = bt2.IntegerFieldType(32)(17)
str_field = bt2.StringFieldType()('hello there')
Features:
* Copy, deep copy, and comparison support.
* IntegerField and EnumerationField act just like Python's int.
* FloatingPointNumberField acts just like Python's float.
* StringField acts just like Python's str.
* ArrayField and SequenceField honor the mutable sequence protocol.
* StructureField honors the mutable mapping protocol.
Field objects are just like value objects: they act like native Python
objects:
int_field = bt2.IntegerFieldType(32)(152)
int_field += 194
print(int_field % 51)
str_field = bt2.StringFieldType()('hello there')
print(len(str_field))
str_field += ' World!'
print(str_field)
print(struct_field['oh']['noes'][23])
print(variant_field.selected_field)
for mapping in enum_field.mappings:
print(mapping.name, mapping.lower, mapping.upper)
clock_class
-----------
bt_ctf_clock_class API.
A straightforward clock class wrapper, pretty much equivalent to the
previous one (CTFWriter.Clock), except that:
* Copy, deep copy, and comparison support.
* You can pass all the properties of an object at construction time:
cc = bt2.ClockClass('my_clock', frequency=
18000000,
is_absolute=True, precision=500,
offset=bt2.ClockClassOffset(seconds=22,
cycles=187232))
* A clock offset is represented with a ClockClassOffset object.
* You can create a clock value from a clock class object with a given
number of cycles:
clock_val = cc.create_clock_value(234)
This clock value object is copyable, deep-copyable, and comparable.
You can get its number of cycles (raw value), its clock class,
and the number of nanoseconds since Epoch:
print(clock_val.ns_from_epoch())
event_class
-----------
bt_ctf_event_class API.
Features:
* Copy, deep copy, and comparison support.
* You can pass all the properties of an object at construction time:
ec = bt2.EventClass('my_event', id=23, payload_field_type=ft)
* Parent stream class access (returns None if not set):
print(ec.stream_class.id)
* Attributes property which honor the mutable mapping protocol:
event_class.attributes['model.emf.uri'] = 'http://diamon.org/'
* Payload and context field type R/W properties.
* Call the class to instantiate an event:
my_event = my_event_class()
stream_class
------------
bt_ctf_stream_class API.
Features:
* Copy, deep copy, and comparison support.
* You can pass all the properties of an object at construction time:
sc = bt2.StreamClass(name='my_stream_class',
event_header_field_type=ev_header_ft,
event_classes=(ec1, ec2, ec3))
* Parent trace access (returns None if not set):
print(sc.trace)
* A stream class object honors the mapping protocol to access its
event class children by name:
ec = sc['my_event']
for ec_name, ec in sc.items():
print(ec_name, ec.id)
* Packet context, event header, and stream event context field type R/W
properties.
* Call the class to instantiate a stream:
my_stream = my_stream_class('optional_name')
trace
-----
bt_ctf_trace API.
Features:
* Copy, deep copy, and comparison support.
* You can pass all the properties of an object at construction time:
trace = bt2.Trace(name='my_trace',
native_byte_order=bt2.ByteOrder.LITTLE_ENDIAN,
env={'tracer_name': 'BestTracer', 'custom': 23},
packet_header_field_type=pkt_head_ft,
clock_classes=(cc1, cc2),
stream_classes=(sc1, sc2))
* A trace object honors the mapping protocol to access its stream
class children by ID:
sc = trace[23]
for sc_id, sc in trace.items():
print(sc_id, len(sc))
* Trace environment honors the mutable mapping protocol:
trace.env['tracer_major'] = 1
trace.env['tracer_minor'] = 2
trace.env['uname_r'] = '4.7.2-1-ARCH'
for k, v in trace.env.items():
print(k, v)
* Trace clock classes honor the mapping protocol:
cc = trace.clock_classes['my_clock']
for cc_name, cc in trace.clock_classes.items():
print(cc_name, cc.frequency)
* Packet header field type R/W property.
event
-----
bt_ctf_event API.
Features:
* Copy, deep copy, and comparison support.
* Event class, name, ID, and stream read-only properties.
* Event object implements __getitem__() to retrieve a field in different
scopes:
# can be found in context, if not in payload, for example
my_event['cpu_id']
This is the same behaviour as in the `babeltrace` package. Use the
specific field properties instead of a field_with_scope() method:
print(my_event.context_field['specific'])
* Packet property to set and get the event's packet:
event.packet = my_packet
* Header, stream event context, context, and payload field R/W
properties.
* You can assign a clock value mapped to a specific clock class and
get it back:
event.set_clock_value(some_clock_value)
print(event.get_clock_value(some_cc).ns_from_epoch)
stream
------
bt_ctf_stream API.
This module defines a base class (_StreamBase) for _Stream (non-writer
stream) and _CtfWriterStream (writer stream).
Features:
* Copy, deep copy, and comparison support.
* You can create a packet from a non-writer stream:
packet = stream.create_packet()
packet
------
bt_ctf_packet API.
Features:
* Copy, deep copy, and comparison support.
* Stream read-only property (gives back the stream object which
created it).
* Packet context and packet header field R/W properties.
notification
------------
bt_notification API.
The classes in this module wrap their equivalent in the C API in a
pretty straightfoward way.
notification_iterator
---------------------
bt_notification_iterator API.
A notification iterator object is always created from a source/filter
component object:
source_component.create_notification_iterator()
A notification iterator object has a next() method to go to the next
notification, and a `notification` property to get the current
notification:
notif_iter.next()
print(notif_iter.notification)
The next() method can raise:
* bt2.Stop: End of the iteration (inherits StopIteration).
* bt2.UnsupportedFeature: Unsupported feature.
* bt2.Error: Any other error.
A notification iterator also honors the iterator protocol, that is, you
can use it like any Python iterator:
for notif in notif_iter:
print(notif)
Note that the iteration can still raise bt2.UnsupportedFeature or
bt2.Error in this scenario (bt2.Stop stops the iteration: it's not
raised outside the iteration).
You can use the seek_to_time() method to make a notification iterator
seek to a specific time.
The `component` property of a notification iterator returns the original
source/filter component which was used to create it.
You can create your own notification iterator class (to be used by your
own source/filter component class) by inheriting
bt2.UserNotificationIterator. This asks you to write your own _next()
and _get() methods which are eventually called by
bt_notification_iterator_next() and
bt_notification_iterator_get_notification(). You can also define an
__init__() method, a _destroy() method, and a _seek_to_time() method.
Minimal user notification iterator class:
class MyIterator(bt2.UserNotificationIterator):
def _get(self):
# ...
def _next(self):
# ...
Your _next() method can raise bt2.Stop to signal the end of the
iteration. If it raises anything else, bt_notification_iterator_next()
returns BT_NOTIFICATION_ITERATOR_STATUS_ERROR to its caller. Since
the C API user only gets a status from this function, any exception
value is lost during this translation. However the C next method could
still log this value and the traceback in verbose mode.
Your _get() method can raise anything so that the caller of
bt_notification_iterator_get_notification() gets an error status.
Complete user notification iterator class:
class MyIterator(bt2.UserNotificationIterator):
def __init__(self):
# Called when the user calls
# bt_component_source_create_notification_iterator() or
# bt_component_filter_create_notification_iterator().
# Anything you raise here makes this function return
# NULL (creation error).
def _get(self):
# ...
def _next(self):
# ...
def _seek_to_time(self, origin, time):
# You can raise anything or bt2.UnsupportedFeature.
# `origin` is one of the values of
# bt2.NotificationIteratorSeekOrigin.
def _destroy(self):
# This is called when the actual native BT notification
# iterator object is destroyed. Anything you raise here
# is ignored. You cannot use __del__() for this for
# implementation reasons.
You CANNOT manually instantiate a user notification iterator, e.g.:
my_iter = MyIterator()
This makes no sense because a notification iterator is always created by
a source/filter component. It probably won't work anyway because
bt2.UserNotificationIterator.__new__() expects a SWIG pointer and your
__init__() probably does not.
component
---------
bt_component_class and bt_component APIs.
This is where the fun begins.
All component objects have the following properties:
* name: Component's name or None.
* component_class: Component class object.
Source components have:
* create_notification_iterator(): Returns a new notification
iterator object.
Filter components have:
* create_notification_iterator(): Returns a new notification
iterator object.
* add_notification_iterator(): Adds a notification iterator to the
filter component.
Sink components have:
* consume(): Consumes notifications from its input notification
iterators.
* add_notification_iterator(): Adds a notification iterator to the
filter component.
A component class object has the following properties:
* name: Component class's name or None.
* description: Component class's description or None.
You can also call a component class object to create a component
object, with optional parameters and an optional component name:
comp = comp_class(name='my_comp', params={'path': '/tmp/lel'})
The `params` argument is passed to bt2.create_value() so you can use
a direct *Value object or anything accepted by this utility function.
What is described above is the _generic_ part of components and
component classes. There's another part for user-defined component
classes. For a user of those classes, both generic and user-defined
classes expose the same interface. The relative complexity of how this
is achieved is justified by the great simplicity from the component
developer's perspective:
class MySink(bt2.UserSinkComponent):
def _consume(self):
notif_iter = self._input_notification_iterators[0]
notif = next(notif_iter)
if isinstance(notif, bt2.TraceEventNotification):
print(notif.event.name)
That's it: some kind of minimal sink component class. Note that
next(notif_iter) can raise bt2.Stop here which is passed to the eventual
caller of bt_component_sink_consume() as the BT_COMPONENT_STATUS_END
status.
Behind the scenes, bt2.UserSinkComponent uses _UserComponentType as its
metaclass. When the class itself is initialized, its metaclass checks if
the subclass has the required interface (depending on its base class,
bt2.UserSinkComponent in this case) and creates a bt_component_class
owned by the Python user class. This bt_component_class is associated to
the Python class thanks to a global GHashTable in the shared object
module (_native_bt.so). Both the key and the value are weak references.
The name of the created bt_component_class is the user class's name by
default (MySink above), but it can also be passed as a class argument.
The description of the created bt_component_class is the docstring of
the user class:
class MySink(bt2.UserSinkComponent, name='another-name'):
'this is a custom sink'
def _consume(self):
# ...
Source and filter user component classes need to specify a notification
iterator class to use when the user calls
bt_component_*_create_notification_iterator(). This is specified as
a class argument.
class MyIterator(bt2.UserNotificationIterator):
def __init__(self):
# ...
def _get(self):
# ...
def _next(self):
# ...
def _seek_to_time(self, origin, time):
# ...
def _destroy(self):
# ...
class MySource(bt2.UserSinkComponent,
notification_iterator_class=MyIterator):
# no mandatory methods here for a source/filter component class
Note that, within the notification iterator methods, self.component
refers to the actual user Python object which was used to create the
iterator object. This is the way to access custom, component-wide data
when the notification iterator is created (self.component._whatever).
Optional methods for all user-defined component classes are:
class AnyComponent(...):
def __init__(self, params, name):
# `params` is a *Value Python object (bt2.values module),
# `name` is the optional (can be None) component name,
# which you can also access as self.name at this point.
def _destroy(self):
# This is called when the actual native BT component
# object is destroyed. Anything you raise here
# is ignored. You cannot use __del__() for this for
# implementation reasons.
Optional methods for filter and sink user-defined component classes
are:
class FilterOrSinkComponent(...):
def _add_notification_iterator(self, notif_iter):
# This is called when a notification iterator is added to
# the component (using bt_component_*_add_iterator()).
Additionally, the __init__() method of filter and sink component classes
can use the self._minimum_input_notification_iterator_count and
self._maximum_input_notification_iterator_count properties to set their
minimum and maximum number of allowed input notification iterators:
class FilterOrSinkComponent(...):
def __init__(self, params, name):
self._maximum_input_notification_iterator_count = 10
self._minimum_input_notification_iterator_count = 4
They can also use the self._input_notification_iterators property at the
appropriate time to get their connected input notification iterators.
This property honors the sequence protocol. For filter components, this
is most probably going to be used by the iterator class, as such:
class MyIterator(bt2.UserNotificationIterator):
def _get(self):
# ...
def _next(self):
notif_iter = self.component._input_notification_iterators[0]
# ...
The beauty of all this is that both a Python user and the C API side can
instantiate user components:
Python:
my_sink = MySink(params={'janine': 'sutto'})
for _ in my_sink.consume():
pass
C API (provided you have access to the bt_component_class object
created for the Python user class):
my_sink = bt_component_create(my_sink_comp_class, NULL, params);
while (true) {
status = bt_component_sink_consume(my_sink);
if (status == BT_COMPONENT_STATUS_END) {
break;
}
}
This is possible thanks to the overridden metaclass's __call__() method:
* When a Python user instantiates a user-defined component class, the
metaclass's __call__() method creates an uninitialized user component
and calls bt_component_create_with_init_method_data(), giving to this
function `self` (the uninitialized component).
When the component initialization method is called with some init
method data, it sets the bt_component pointer in the received Python
object and calls its __init__() method so that its intialization
happens within the bt_component_create_with_init_method_data() call
(important because some functions cannot be called outside this
function).
If the user's __init__() method raises, the error is not cleared on
the C side, so that the Python user who instantiates the component
can catch the actual, original Python exception instead of getting
a generic one.
In this scenario, the created user component Python object OWNS its
bt_component. The component is marked as NOT being owned by its
bt_component:
self._belongs_to_native_component = False
The bt_component has the user Python object as its private data
(borrowed reference).
* When a C user instantiates a user-defined Python component class, he
calls bt_component_create(). Then the component initialization
function for this class receives a NULL init method data and knows
it is called from the C/generic side.
The initialization method finds the corresponding Python component
class thanks to the aforementioned global GHashTable. It calls it with
the `__comp_ptr` keyword argument set to the native bt_component SWIG
pointer and the `params` keyword argument set to the *Value object
converted from the `params` parameter. This call (metaclass's
__call__()), in this scenario, calls the user's __init__() method
itself. This call returns a new component instance, which is set as
the private data of the native bt_component.
In this scenario, the created user component Python object as a
borrowed reference to the native bt_component. The native bt_component
OWNS the Python user's component:
self._belongs_to_native_component = True
The self._belongs_to_native_component property is used for the following
situation:
my_source = MySource()
# At this point, my_source is a Python object which owns its
# bt_component.
notif_iter = my_source.create_notification_iterator()
# notif_iter is a generic notification iterator (a dumb
# bt_notification_iterator wrapper) here, not the actual user
# Python notification iterator. This method only calls
# bt_component_source_create_notification_iterator() and wraps the
# returned pointer.
del my_source
# At this point, the Python reference count of the source component
# object falls to zero. Its __del__() method is called. However
# we don't want this object to be destroyed here, because it is
# still needed by the user notification iterator. This __del__()
# method, if self._belongs_to_native_component is false, inverts
# the ownership, literally:
#
# if not self._belongs_to_native_component:
# self._belongs_to_native_component = True
# native_bt.py3_component_on_del(self)
# native_bt.put(self._ptr)
#
# bt_py3_component_on_del() simply increments the given
# Python object's reference count. With its reference count back
# to 1, Python does not actually destroy the object. It is now
# owned by the bt_component.
del notif_iter
# Now, the wrapper puts its bt_notification_iterator object. Its
# reference count falls to zero. Its bt_component is put: its
# reference count falls to zero. The user's (C) destroy method for
# this component class decrements the reference count of its
# private Python object (the same object referenced by my_source
# above). __del__() is (possibly) called again, but is a no-op
# now. Then the user's _destroy() method is called.
plugin
------
bt_plugin API.
You can create plugin objects with bt2.create_plugins_from_file().
This is the equivalent of bt_plugin_create_all_from_file(). You can
also use bt2.create_plugins_from_dir() which is the equivalent of
bt_plugin_create_all_from_dir().
The return value of those functions is a list of _Plugin objects.
Here's an example of printing all the event names of a CTF trace:
import bt2
import sys
def print_all():
plugins = bt2.create_plugins_from_file(sys.argv[1])
fs_cc = plugins[0].source_component_class('fs')
fs_comp = fs_cc(params={'path': sys.argv[2]})
notif_iter = fs_comp.create_notification_iterator()
for notif in notif_iter:
if isinstance(notif, bt2.TraceEventNotification):
print(notif.event.name)
print_all()
You would run this script like this:
python3 script.py /path/to/ctf-plugin.so /path/to/trace
You can access the properties of a plugin:
print(plugin.path)
print(plugin.name)
print(plugin.description)
print(plugin.author)
print(plugin.license)
print(plugin.version)
A plugin object honors the sequence protocol:
for comp_class in plugin:
print(comp_class.name, comp_class.description)
ctf_writer
----------
Everything in this module is exclusive to the CTF writer API:
* CtfWriterClock (bt_ctf_clock)
* _CtfWriterStream (bt_ctf_stream, CTF writer interface only)
* CtfWriter (bt_ctf_writer)
I removed the CTFWriter.Writer.create_stream() method because it's the
equivalent of this:
writer.trace.add_stream_class(sc)
stream = sc()
This returns a _CtfWriterStream object.
Also removed is CTFWriter.Writer.add_environment_field() which you can
do like this now:
writer.trace.env['name'] = value
The CTFWriter.Writer.byte_order property is now the `native_byte_order`
property of the CTF writer's trace:
writer.trace.native_byte_order = bt2.ByteOrder.BIG_ENDIAN
CtfWriter.add_clock() expects a CtfWriterClock object.
__init__
--------
This imports * from each module, thus exposing only the public names of
each one.
It also defines the package's exceptions. bt2.CreationError is raised
when an object cannot be created. bt2.FrozenError is raised when an
operation fails because the object is frozen. This is only raised for
bt2.values objects since this API has a status to indicate the exact
error. bt2.Error is a general error.
The package also does this:
import bt2.native_bt as _native_bt
import atexit
atexit.register(_native_bt.py3_cc_exit_handler)
_native_bt.py3_cc_init_from_bt2()
bt_py3_cc_init_from_bt2() is used to import some bt2 modules and objects
on the C side and the exit handler, bt_py3_cc_exit_handler(), puts those
objects.
Signed-off-by: Philippe Proulx <eeppeliteloop@gmail.com>
Signed-off-by: Jérémie Galarneau <jeremie.galarneau@efficios.com>
Signed-off-by: Jérémie Galarneau <jeremie.galarneau@efficios.com>