diff --git a/README.md b/README.md index 18eed63..2a240bd 100644 --- a/README.md +++ b/README.md @@ -3,64 +3,66 @@ Download the release for your platform from the Releases page and unpack it somewhere. For building from source, see the instructions below. ## CMake Setup + Given a C++ library that we want to bind, the thing we need to generate is a C wrapper library that provides C types and function definitions that will call their corresponding C++ functions and methods. In order to do that we use CMake to run `bbl-translate`, which takes a set of bind files as input and outputs C source code, then compiles the generated source into a library we can use. A wrapper library's CMakeLists.txt looks like the below. This is the CMakeLists.txt for [bbl-usd](https://github.com/anderslanglands/bbl-usd) which binds [Pixar's OpenUSD](https://github.com/PixarAnimationStudios/OpenUSD). + ```cmake cmake_minimum_required(VERSION 3.15) project(bbl-usd VERSION 0.1 LANGUAGES C CXX) -# babble should be found by adding the directory where you installed babble to +# babble should be found by adding the directory where you installed babble to # the CMAKE_PREFIX_PATH variable find_package(babble CONFIG REQUIRED) find_package(pxr REQUIRED) # The bindfile contains all our binding definitions set( - bindfiles - bind/ar.cpp - bind/gf.cpp - bind/js.cpp - bind/pcp.cpp - bind/sdf.cpp + bindfiles + bind/ar.cpp + bind/gf.cpp + bind/js.cpp + bind/pcp.cpp + bind/sdf.cpp bind/std.cpp - bind/tf.cpp - bind/usd/geom.cpp - bind/usd/prim.cpp - bind/usd/schema.cpp - bind/usd/usd.cpp - bind/vt.cpp + bind/tf.cpp + bind/usd/geom.cpp + bind/usd/prim.cpp + bind/usd/schema.cpp + bind/usd/usd.cpp + bind/vt.cpp ) -# bbl_translate_binding sets up the invocation of bbl-translate to generate the -# wrapper library source and creates the target called `usd-c`, which we can then +# bbl_translate_binding sets up the invocation of bbl-translate to generate the +# wrapper library source and creates the target called `usd-c`, which we can then # add definitions, links etc to as a normal CMake library target. -# The first argument is the project name, from which the names of all the generated +# The first argument is the project name, from which the names of all the generated # libraries will be created # by appending the language name as `usd-c`, `usd-rust` etc. bbl_translate_binding( openusd - BINDFILES + BINDFILES ${bindfiles} - COMPILE_ARGS - -Wno-deprecated-builtins - -DNOMINMAX - -D_MT - -DBOOST_ALL_NO_LIB - -D__TBB_show_deprecation_message_task_H + COMPILE_ARGS + -Wno-deprecated-builtins + -DNOMINMAX + -D_MT + -DBOOST_ALL_NO_LIB + -D__TBB_show_deprecation_message_task_H ) -# bbl_translate_binding creates a library target in the form ${PROJECT_NAME}-c +# bbl_translate_binding creates a library target in the form ${PROJECT_NAME}-c # where PROJECT_NAME is the first argument to the function. # We then add links, defs etc to the target as usual target_link_libraries(openusd-c PUBLIC usd sdf js usdGeom) target_compile_definitions( - openusd-c - PRIVATE - NOMINMAX - BOOST_ALL_NO_LIB + openusd-c + PRIVATE + NOMINMAX + BOOST_ALL_NO_LIB __TBB_show_deprecation_message_task_H ) @@ -75,26 +77,29 @@ target_link_libraries(usd-c-test01 PUBLIC openusd-c) target_include_directories(usd-c-test01 PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) set_property(TARGET usd-c-test01 PROPERTY C_STANDARD 99) -# This is just here to trigger generation of compile commands for the bindfile so -# we get LSP functionality in the bind file. LSP completion of function names can +# This is just here to trigger generation of compile commands for the bindfile so +# we get LSP functionality in the bind file. LSP completion of function names can # be a huge time saver when authoring bindfiles. add_library(bind-dummy ${bindfiles}) target_link_libraries(bind-dummy babble::bind) target_include_directories(bind-dummy PRIVATE $) ``` -Then when CMake is run as usual: +Then when CMake is ran as usual: + ```sh cmake -B build -G Ninja -DCMAKE_PREFIX_PATH=";;" -DCMAKE_BUILD_TYPE=Release cmake --build build --config Release ``` + The wrapper library will be built as well as in this case the C99 test program that uses it. ## Bindfiles Bindfiles are C++ source files that contain definitions that allow `bbl-translate` to extract all the necessary information about types that are to be bound using clang. Their structure is somewhat similar to pybind, but they work very differently as instead of compiling an actual library that is then loaded in some other application, the bindfiles are just used to contstruct the AST in clang, and the AST is then translated to a C API by code generation. -### Simple example +### Simple Example + For example, given a simple C++ library: ```c++ @@ -124,6 +129,7 @@ BBL_MODULE(foo) { ``` This would generate the following C API: + ```c /// \brief Vary Barry typedef struct foo_Bar_t foo_Bar_t; @@ -140,9 +146,11 @@ Let's break down what's happening here piece by piece. ```c++ #include ``` + The `babble` header contains the types we'll use to bind various different constructs, for example `bbl::Class`, and is required for all bindings. ### `BBL_MODULE` + ```c++ BBL_MODULE(foo) { ``` @@ -151,7 +159,8 @@ This opens a new **module**, which in the case of a C library currently just spe Other language plugins may choose to do something more with modules (for example to split the generated libraries into multiple pieces). -### `bbl:Class` - Class Binding +### `bbl:Class` – Class Binding + ```c++ bbl::Class() ``` @@ -161,10 +170,13 @@ This binds the class (or struct) `foo::Bar`, meaning that a C struct will be gen #### Renaming The `bbl::Class` constructor can optionally take a string argument that renames the generated struct. For instance, if we instead did: + ```c++ bbl::Class("Baz") ``` + The generated C would be: + ```c typedef struct foo_Baz_t foo_Baz_t; int foo_Baz_set_baz(foo_Baz_t* _this, float b); @@ -172,11 +184,13 @@ int foo_Baz_get_baz(foo_Baz_t const* _this, float* result); ``` This is useful in particular for renaming templated types, or removing common prefixes from type names that are redundant under the automatic module prefixing. For instance: + ```c++ bbl::Class>("StringVector"); ``` #### Binding Value Types + By default, babble will bind all types as *opaque pointers*. As can be seen in the example above, this means an undefined struct is created for the type and it is passed through the wrapper as a pointer, so the C code never knows anything about the type. This is of course important because C++ contains many constructs that cannot be represented in C, so being able to see the innards of a `std::vector` makes no sense. @@ -194,6 +208,7 @@ struct Point2D { } ``` + ```c++ // bind file BBL_MODULE(math) { @@ -204,6 +219,7 @@ BBL_MODULE(math) { ; } ``` + ```c // generated header struct math_Point2D_t { @@ -211,14 +227,17 @@ struct math_Point2D_t { float y; }; ``` -Note that the `value_type()` method call on the `bbl::Class` binding tells babble that this type should be bound as a value type, and the fields are bound individually with the `f()` method call. -Currently, no checking is done to ensure that it is safe to bind types as value types, so it is up to you to make sure there are no sneaky move constructors or destructors that would cause undefined behaviour in the generated library. +Note that the `value_type()` method call on the `bbl::Class` binding tells babble that this type should be bound as a value type, and the fields are bound individually with the `f()` method call. + +Currently, no checking is done to ensure that it is safe to bind types as value types, so it is up to you to make sure there are no sneaky move constructors or destructors that would cause undefined behaviour in the generated library. Static asserts are generated in the wrapper library so that at least it can be ensured that the generated value type and the target C++ type have the same size and alignment, and the generated wrapper library will fail to compile if this is not the case. #### Replacement Types + In some rare cases, a type *could be* a value type, but cannot be bound as such because the target library authors decided to make the fields private. For instance, if we change the declaration of `Point2D` from the previous example to a class instead of a struct, now all its fields are private and we can no longer bind it as a value type: + ```c++ // Target library header namespace foo { @@ -228,9 +247,11 @@ class Point2D { }; } ``` -Now, the library authors *probably* had a good reason for doing this, but if you're sure it's safe to do so, you may wish to replace this type with an ABI-compatible struct that exposes those members directly to avoid a whole lot of `set_x()`/`get_y()` tomfoolery. + +Now, the library authors *probably* had a good reason for doing this, but if you're sure it's safe to do so, you may wish to replace this type with an ABI-compatible struct that exposes those members directly to avoid a whole lot of `set_x()`/`get_y()` tomfoolery. If you're *really, really sure* you can do this, then you achieve it like so: + ```c++ // bindfile struct MyPoint2D { @@ -244,11 +265,13 @@ BBL_MODULE(math) { ; } ``` -Which will generate the same `math_Point2D_t` struct as in the previous example. +Which will generate the same `math_Point2D_t` struct as in the previous example. #### Inheritance + Methods from superclasses are added to subclasses automatically when the superclasses are themselves bound (otherwise they cannot be extracted): + ```c++ namespace foo { class Base { @@ -267,6 +290,7 @@ namespace foo { }; } ``` + ```c++ // bindfile BBL_MODULE(foo) { @@ -279,6 +303,7 @@ BBL_MODULE(foo) { ; } ``` + ```c // generated header typedef struct foo_Base_t foo_Base_t; @@ -289,11 +314,13 @@ int foo_Base_do_base_thing(foo_Base_t* _this); int foo_Derived_do_base_thing(foo_Derived_t* _this); int foo_Derived_do_derived_thing(foo_Derived_t* _this); ``` -Note that `Derived`'s wrapper automatically picks up `do_base_thing()` from `Base` but not `do_other_base_thing()` form `OtherBase` because `Base` is bound but `OtherBase` is not. +Note that `Derived`'s wrapper automatically picks up `do_base_thing()` from `Base` but not `do_other_base_thing()` form `OtherBase` because `Base` is bound but `OtherBase` is not. #### Binding Smart Pointers + Consider: + ```c++ namespace foo { class Bar; @@ -308,6 +335,7 @@ namespace foo { }; } ``` + One way to handle this would just be to bind the `get()` method on `std::unique_ptr` and then use the returned `Bar*` in client code. This can be a little long-winded though and encourages separating the type you're using (`Bar*`) from the type expressed in the target library (`BarPtr`), which makes it more likely you'll forget to call the destructor on `BarPtr`, leading to memory leaks and other shenanigans. You can specify that a type is a smart pointer to another type using the `smartptr_to()` method on `bbl::Class`. This causes all methods from the "to" type to be added to the smart pointer type automatically: @@ -346,29 +374,35 @@ int foo_BarPtr_get_baz(foo_BarPtr_t const* _this, float* result); int foo_ConstBarPtr_get_baz(foo_ConstBarPtr_t const* _this, float* result); ``` + Note that because `ConstBarPtr` was declared with a `const` template argument in `smartptr_to()`, it does not get the non-const methods from `Bar`. Smart pointers also automatically receive methods from all the targeted type's superclasses if those superclasses are themselves bound. +### `.m()` – Method Binding -### `.m()` - Method Binding ```c++ .m(&foo::Bar::set_baz) ``` + The `m()` method on `bbl::Class` specifies a method to be bound and takes a member function pointer. From this, babble can extract everything it needs to know about the method in question, so a lot of the time, this is all you need to do. Like the `bbl::Class` constructor, `m()` takes an optional second parameter that specifies a rename string for the method, which will rename the generated function in the C wrapper. For instance: + ```c++ .m(&foo::Bar::set_baz, "SetQux") ``` + would generate: + ```c int foo_Baz_SetQux(foo_Baz_t* _this, float b); ``` This is particularly useful in the case of overloaded methods (see below). -#### Exception handling +#### Exception Handling + The attentive reader will have noticed that the return value in `foo::Bar::get_baz()` has moved to an out parameter, and all generated functions return `int`. This is because all generated wrapper functions return an integer error code, and return values in C++ are always moved to out parameters. The return codes are there in order to handle exceptions. The C wrapper will catch all exceptions and return a non-zero value in that case. Success is always indicated by a return value of zero. @@ -378,7 +412,9 @@ If a function or method is marked as `noexcept` then the exception-handling mach Currently, any caught exception will generate a return code of `1`, but future versions of babble will allow specifying unique error codes per exception type. #### Overloaded Methods + Consider: + ```c++ namespace foo { class Bar { @@ -415,7 +451,9 @@ BBL_MODULE(foo) { ``` ### Template Methods + Template methods must be bound separately for each type you wish to instantiate them with. For instance: + ```c++ // target library header namespace foo { @@ -439,7 +477,9 @@ BBL_MODULE(foo) { ``` ### Constructors + You cannot create a member function pointer to a constructor in C++, so they must be specified slightly differently: + ```c++ // target library header namespace foo { @@ -450,6 +490,7 @@ namespace foo { }; } ``` + ```c++ // bindfile BBL_MODULE(foo) { @@ -459,12 +500,14 @@ BBL_MODULE(foo) { ; } ``` + ```c // generated wrapper header typedef struct foo_Bar_t foo_Bar_t; int foo_Bar_default(foo_Bar_t** result); int foo_Bar_from_float(float value, foo_Bar_t** result); ``` + We bind constructors using the `ctor()` method on `bbl::Class`. The `ctor()` method takes an instance of `bbl::Class::Ctor`. The template arguments to `bbl::Class::Ctor` define the types of the arguments to the constructor. @@ -473,23 +516,28 @@ The parameters to the `bbl::Class::Ctor` constructor optionally define the names Finally, the last argument to `ctor()` is an optional rename for the constructor function itself. -### Copy constructors and destructors +### Copy Constructors & Destructors + Copy constructors and destructors are automatically generated when appropriate. You do not need to do anything to enable this. -## `bbl::fn - Binding Functions` +## `bbl::fn` – Binding Functions + Functions are bound in much the same way as methods, using the `bbl::fn` call: + ```c++ // target library header namespace foo { void do_thing(); } ``` + ```c++ // bindfile BBL_MODULE(foo) { bbl::fn(&foo::do_thing) } ``` + ```c // generated wrapper header int foo_do_thing(); @@ -497,8 +545,10 @@ int foo_do_thing(); As with the `m()` method on `bbl::Class`, `fn()` takes an optional second parameter that is the rename string for the function. -### Extension functions +### Extension Functions + It is also possible to bind your own functions to be added to the generated wrapper library. Any functions declared in the `bblext` namespace can be bound as normal and will be treated as if they had been present in the target library. For instance: + ```c++ // bindfile namespace bblext { @@ -511,6 +561,7 @@ BBL_MODULE(foo) { bbl::fn(&bblext::my_do_thing, "do_thing"); } ``` + ```c // generated wrapper source int foo_do_thing(float bar) { @@ -520,12 +571,14 @@ int foo_do_thing(float bar) { ``` This can be particularly useful for tweaking the interface of the target library to be more C-friendly. For instance, it is a common pattern in C++ for functions to take a `std::string const&` parameter: + ```c++ // target library header namespace foo { void hello(std::string const& msg); } ``` + ```c++ // bindfile BBL_MODULE(foo) { @@ -536,6 +589,7 @@ BBL_MODULE(foo) { ; } ``` + ```c // generated wrapper header int foo_hello(foo_String_t const* msg); @@ -543,18 +597,23 @@ int foo_hello(foo_String_t const* msg); typedef struct foo_String_t foo_String_t; foo_String_from_c_str(char const* str, foo_String_t** result); ``` + Using this API in client code then entails: + ```c foo_String_t* world; foo_String_from_c_str("World!", &world); foo_hello(world); ``` + which is annoying, when what you really want to do is simply: + ```c foo_hello("World!"); ``` We can provide this API by not binding `foo::hello()` directly, but by binding an extension function instead that just takes the `char const*` and calls `foo::hello()` with it: + ```c++ // bindfile namespace bblext { @@ -567,7 +626,9 @@ BBL_MODULE(foo) { bbl::fn(&bblext::hello); } ``` + which will generate: + ```c // generated wrapper header foo_hello(char const* msg); @@ -575,10 +636,12 @@ foo_hello(char const* msg); As of babble 0.5, it's possible to do this directly inline in the binding with... -## `bbl::Wrap` - inline lambda wrappers +## `bbl::Wrap` – Inline Lambda Wrappers + We can generate the exact same result as manually creating an extension function, but without having to write or bind that function by using `bbl::Wrap`. Given a similar C++ target from the previous examples: + ```c++ // target library header namespace foo { @@ -589,7 +652,9 @@ namespace foo { }; } ``` + we can do the replacement of `std::string const&` with `char const*` directly inline in the bindings using `bbl::Wrap`: + ```c++ // bindfile BBL_MODULE(foo) { @@ -605,9 +670,10 @@ BBL_MODULE(foo) { } ``` +## `bbl::Enum` – Binding Enums -## `bbl::Enum` - Binding Enums Enums are bound similarly to classes: + ```c++ // target library header namespace foo { @@ -618,12 +684,14 @@ namespace foo { }; } ``` + ```c++ // bind file BBL_MODULE(foo) { bbl::Enum(); } ``` + ```c // generated wrapper header typedef enum foo_Axis_t { @@ -634,7 +702,9 @@ typedef enum foo_Axis_t { ``` ### Enum Prefixes + By default, the enum's variants will be prefixed with the qualified name of the enum itself, to avoid collisions. If you want to change the prefix (or remove it), use the `prefix()` method on `bbl::Enum`: + ```c++ // bind file BBL_MODULE(foo) { @@ -643,6 +713,7 @@ BBL_MODULE(foo) { ; } ``` + ```c // generated wrapper header typedef enum foo_Axis_t { @@ -651,11 +722,13 @@ typedef enum foo_Axis_t { AXIS_Z = 2, } foo_Axis_t; ``` + Passing an empty string to `prefix()` will remove the prefix completely. +## `std::function` Callbacks -## std::function callbacks Functions that take a callback in the form of a `std::function` can be bound by binding the `std::function` explicitly with `bbl::Class`. Consider this library that defines a class, `Foo` with member accessors, and another class `Bar`, which has a member `adjust_foo()` that takes a `FooFn` callback in order to modify an internal `Foo` object: + ```c++ // target library header namespace tst { @@ -684,7 +757,9 @@ public: }; } ``` + Here, `Bar::adjust_foo` takes a callback to modify the value of a `Foo` before printing it. This can be bound like so: + ```c++ // bindfile BBL_MODULE(tst) { @@ -708,7 +783,9 @@ BBL_MODULE(tst) { } ``` + Which generates the following wrapper API: + ```c typedef struct tst_Foo_t tst_Foo_t; typedef struct tst_Bar_t tst_Bar_t; @@ -717,9 +794,11 @@ int tst_Bar_adjust_foo(void (*fn)(tst_Foo_t* param00, tst_Foo_t** _result)); // ... snip ``` + Note that the C version of `adjust_foo` takes a function pointer that matches the signature of the bound `std::function`, after having the same transformations applied as any other C++ function signature, e.g. the return parameter is moved to an out parameter. The generated wrapper can be used in client code like so: + ```c // client code @@ -732,38 +811,44 @@ void fn(tst_Foo_t* foo, tst_Foo_t** result) { int main() { // adjust_foo now takes the function pointer with the transformed signature tst_Bar_adjust_foo(fn); - return 0; + return 0; } ``` Which will result in the following output when compiled and run: -``` + +```text foo is now 42 ``` -Note that currently, only callbacks implemented as `std::function` can be handled this way. Regular function pointers do not work. In the future, limited support for function pointers will be added. The limitation will be that only function pointers whose signatures consist solely of builtins and value types can be bound. +Note that currently, only callbacks implemented as `std::function` can be handled this way. Regular function pointers do not work. In the future, limited support for function pointers will be added. The limitation will be that only function pointers whose signatures consist solely of builtins and value types can be bound. + +## Subclassing C++ Types in C -## Subclassing C++ types in C TODO... implementation of this still to be done. It will work much the same as the `std::function` callbacks, but passing a "virtual" function table to an automatically generated trampoline struct. -## `bbl::constant` - Binding Constants as #defines +## `bbl::constant` – Binding Constants as `#defines` + TODO... -# Language Plugins +## Language Plugins + babble has a plugin architecture to allow generating wrappers for other languages that call the generated C bindings. This is currently WIP. See `plugins/rust` for an example. -# Using Babble as a Library +## Using Babble as a Library + The target `babble::bbl` is a static, C library that can be used both to write new language plugins, as well as perform the same inspection on the target C++ and generated C APIs of a given target library. `include/bbl-context.h` and `include/bbl-capi.h` are the C API and `include/bbl-context.hpp` and `include/bbl-capi.hpp` are a C++ API wrapping the underlying C API. See the `bbl-translate` source and the language plugins for how to use it. TODO: proper docs +## Building `babble` From Source + +### Prerequisites -# Building babble From Source +#### `clang` -## Prerequisites -### clang Requires at least clang 15. Tested with versions 15 through 17. Either download a binary distribution from the llvm-project releases page if one is available, or build from source (it's not hard, just loooong): ```sh @@ -779,10 +864,12 @@ cmake -S llvm -B build -G Ninja \ cmake --build build --target install --config Release ``` -## Build +### Build + There are some submodules so don't forget `--recursive` or `git submodule update --init` after the fact I tend to use VSCode and the terminal for build/test cycle: + ```sh cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH= cmake --build build -j12 --config Release @@ -793,10 +880,11 @@ On windows, the `RelWithDebInfo` config is set up for a debugging experience tha then open `buildvs/babble.sln` in VS, switch to `RelWithDebInfo` and update your debug working directory and arguments as per the testing section below -## Testing +### Testing Run the automated test suite with: -``` + +```sh python ./run_tests.py ```