Skip to content

Commit

Permalink
Add example of loading at runtime from C (#248)
Browse files Browse the repository at this point in the history
* Add example of loading at runtime from C

* Try windows

* Silence windows warnings

* Typo fix

* README tweaks
  • Loading branch information
WardBrian authored Oct 16, 2024
1 parent a186e7e commit e273c8d
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 15 deletions.
32 changes: 27 additions & 5 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:
workflow_dispatch: {}

env:
CACHE_VERSION: 0
CACHE_VERSION: 1

# only run one copy per PR
concurrency:
Expand Down Expand Up @@ -37,17 +37,39 @@ jobs:
path: ./stan/
key: ${{ runner.os }}-stan-${{ hashFiles('stan/src/stan/version.hpp') }}-v${{ env.CACHE_VERSION }}


- name: Set up PATH for C example
if: matrix.os == 'windows-latest'
run: |
Add-Content $env:GITHUB_PATH "$(pwd)/stan/lib/stan_math/lib/tbb"
Add-Content $env:GITHUB_PATH "$(pwd)/test_models/full"
- name: Build C example (Windows)
if: matrix.os == 'windows-latest'
run: |
cd c-example/
make -j4 example.exe example_runtime.exe
rm ../src/bridgestan.o
echo "Dynamically linked example"
./example.exe
echo "Runtime loading example"
./example_runtime.exe ../test_models/full/full_model.so
- name: Build C example (Unix)
if: matrix.os != 'windows-latest'
run: |
cd c-example/
make example -j4
make example_static
rm ../src/bridgestan.o
rm ../test_models/full/full_model.a
make -j4 example example_static example_runtime
rm ../src/bridgestan.o ../test_models/full/*.a
echo "Dynamically linked example"
./example
echo "Statically linked example"
./example_static
echo "Runtime loading example"
./example_runtime ../test_models/full/full_model.so
shell: bash

# we use the cache here to build the Stan models once for multiple interfaces
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*.a
c-example/example
c-example/example_static
c-example/example_runtime
*.exe
make/local

Expand Down
17 changes: 13 additions & 4 deletions c-example/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,18 @@ BS_ROOT=..
include ../Makefile

MODEL?=full
CC ?= gcc
CC = gcc
# .dll on Windows, .so on Linux
ifeq ($(OS_TAG),windows)
DLL = dll
else
DLL = so
endif

../test_models/$(MODEL)/lib$(MODEL)_model.so: ../test_models/$(MODEL)/$(MODEL)_model.so
cp ../test_models/$(MODEL)/$(MODEL)_model.so ../test_models/$(MODEL)/lib$(MODEL)_model.so
../test_models/$(MODEL)/lib$(MODEL)_model.$(DLL): ../test_models/$(MODEL)/$(MODEL)_model.so
cp ../test_models/$(MODEL)/$(MODEL)_model.so ../test_models/$(MODEL)/lib$(MODEL)_model.$(DLL)

example$(EXE): example.c ../test_models/$(MODEL)/lib$(MODEL)_model.so
example$(EXE): example.c ../test_models/$(MODEL)/lib$(MODEL)_model.$(DLL)
$(CC) -c -I ../src example.c -o example.o
$(LINK.c) -o example$(EXE) example.o -Wl,-rpath ../test_models/$(MODEL) -L ../test_models/$(MODEL) -l$(MODEL)_model
$(RM) example.o
Expand All @@ -28,3 +34,6 @@ example_static$(EXE): example.c ../test_models/$(MODEL)/$(MODEL)_model.a
$(CC) -c -I ../src example.c -o example.o
$(LINK.cpp) -o example_static$(EXE) example.o ../test_models/$(MODEL)/$(MODEL)_model.a $(LDLIBS) $(LIBSUNDIALS) $(MPI_TARGETS) $(TBB_TARGETS)
$(RM) example.o

example_runtime$(EXE): runtime_loading.c
$(CC) -I ../src runtime_loading.c -o example_runtime$(EXE)
39 changes: 34 additions & 5 deletions c-example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ This shows how one could write a C program which calls BridgeStan.
Any compiled language with a C foreign function interface and
the ability to link against C libraries should be able to work similarly.

## Usage with dynamic linking
## Binding a specific model at build time

### Dynamic linking

It is possible to link against the same `name_model.so` object used by the other
BridgeStan interfaces. This creates a dynamic link.
Expand All @@ -26,17 +28,20 @@ It has 1 parameters.

You can change the test model by specifying `MODEL` on the command line.
Models which require data can have a path passed in as the first argument.

```shell
make MODEL=multi example
./example ../test_models/multi/multi.data.json
```

This will output

```
This model's name is multi_model.
It has 10 parameters.
```

### Notes
#### Notes

The basic steps for using with a generic BridgeStan model are

Expand All @@ -48,13 +53,13 @@ The basic steps for using with a generic BridgeStan model are
The Makefile in this folder does that by making a copy.

This dynamic linking will work on Windows, but Windows does not record the paths
of shared libraries in executables. As such, `libNAME_model.so` will need to be
of shared libraries in executables. As such, `libNAME_model.dll` will need to be
in the same folder as the executable, or on your `PATH`.

On all platforms, dynamic linking requires that the original `NAME_model.so` object
On all platforms, dynamic linking requires that the original `libNAME_model.(so|dll)` object
still exist when the executable is run.

## Usage with static linking
### Static linking

The makefile here also shows how to create a `.a` static library using the BridgeStan
source, and then compiling an executable which is independent of the location of the model.
Expand All @@ -70,3 +75,27 @@ Will output the same as the above. Note that some Stan libraries such as TBB
are still dynamically linked.

`MODEL` can also be used to specify which model to statically link.

## Loading a model at runtime

The `runtime_loading.c` file shows how to use `dlfcn.h` on Unix and
`libloaderapi.h` on Windows to load a model at runtime.
This is useful if you want to load a model based on user input, or if you want to
load different models in the same executable.

```shell
make example_runtime
# unlike above, this did not automatically build a model, since it
# was not needed to _build_, but we still need one to _run_ the program
make ../test_models/full/full_model.so
./example_runtime ../test_models/full/full_model.so
```

will output

```
This model's name is full_model.
It has 1 parameters.
```

The same executable can be passed different models without recompiling.
90 changes: 90 additions & 0 deletions c-example/runtime_loading.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#include "bridgestan.h"
#include <stdio.h>

#ifdef _WIN32
// hacky way to get dlopen and friends on Windows

#include <libloaderapi.h>
#include <errhandlingapi.h>
#define dlopen(lib, flags) LoadLibraryA(lib)
#define dlsym(handle, sym) (void*)GetProcAddress(handle, sym)

char* dlerror() {
DWORD err = GetLastError();
int length = snprintf(NULL, 0, "%d", err);
char* str = malloc(length + 1);
snprintf(str, length + 1, "%d", err);
return str;
}
#else
#include <dlfcn.h>
#endif

#if __STDC_VERSION__ < 202000
#define typeof __typeof__
#endif

int main(int argc, char** argv) {
char* lib;
char* data;

// require at least the library name
if (argc > 2) {
lib = argv[1];
data = argv[2];
} else if (argc > 1) {
lib = argv[1];
data = NULL;
} else {
fprintf(stderr, "Usage: %s <library> [data]\n", argv[0]);
return 1;
}

// load the shared library
void* handle = dlopen(lib, RTLD_LAZY);
if (!handle) {
fprintf(stderr, "Error: %s\n", dlerror());
return 1;
}

int major = *(int*)dlsym(handle, "bs_major_version");
int minor = *(int*)dlsym(handle, "bs_minor_version");
int patch = *(int*)dlsym(handle, "bs_patch_version");
fprintf(stderr, "Using BridgeStan version %d.%d.%d\n", major, minor, patch);

// Get function pointers. Uses C23's typeof to re-use bridgestan.h
// definitions. We could also write out the types and not include bridgestan.h
typeof(&bs_model_construct) bs_model_construct
= dlsym(handle, "bs_model_construct");
typeof(&bs_free_error_msg) bs_free_error_msg
= dlsym(handle, "bs_free_error_msg");
typeof(&bs_model_destruct) bs_model_destruct
= dlsym(handle, "bs_model_destruct");
typeof(&bs_name) bs_name = dlsym(handle, "bs_name");
typeof(&bs_param_num) bs_param_num = dlsym(handle, "bs_param_num");

if (!bs_model_construct || !bs_free_error_msg || !bs_model_destruct
|| !bs_name || !bs_param_num) {
fprintf(stderr, "Error: %s\n", dlerror());
return 1;
}

// from here on, the code is exactly the same as example.c

// this could potentially error, and we may get information back about why.
char* err;
bs_model* model = bs_model_construct(data, 123, &err);
if (!model) {
if (err) {
printf("Error: %s", err);
bs_free_error_msg(err);
}
return 1;
}

printf("This model's name is %s.\n", bs_name(model));
printf("It has %d parameters.\n", bs_param_num(model, 0, 0));

bs_model_destruct(model);
return 0;
}
19 changes: 18 additions & 1 deletion docs/languages/c-api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@ BridgeStan's pre-requisites and downloaded a copy of the BridgeStan source code.
Example Program
---------------

An example program is provided alongside the BridgeStan source in :file:`c-example/`.
Two example programs are provided alongside the BridgeStan source in :file:`c-example/`.
Details for building the example can be found in :file:`c-example/Makefile`.

The first assumes you wish to link a specific model into the program,
and the second demonstrates how to load a model at runtime

.. raw:: html

<details>
Expand All @@ -30,6 +33,20 @@ Details for building the example can be found in :file:`c-example/Makefile`.
</details>


.. raw:: html

<details>
<summary><a>Show runtime_loading.c</a></summary>


.. literalinclude:: ../../c-example/runtime_loading.c
:language: c

.. raw:: html

</details>


API Reference
-------------

Expand Down

0 comments on commit e273c8d

Please sign in to comment.