Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add example of loading at runtime from C #248

Merged
merged 5 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it reasonable to add ../test_models/$(MODEL)/lib$(MODEL)_model.$(DLL) here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would cause example_runtime to be re-built more often, which feels like it goes against the pedagogical purpose of the example. Strictly speaking, there is no reason that any bridgestan library needs to have been compiled before this, which is what that dependency would be saying

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

goes against the pedagogical purpose of the example

Right, good point.

Is it reasonable to leave the code suggestion in the Readme as is, then add a sentence or two explaining that example_runtime the executable can be compiled, but not run without a .so?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the readme block:

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

$(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() {
WardBrian marked this conversation as resolved.
Show resolved Hide resolved
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
Loading