diff --git a/.github/workflows/test-everything.yml b/.github/workflows/test-everything.yml
index 2f51916e2ef07e3773bc93247b04588523b3f866..3a829fa6974395922f91756302b58f85fc35eb98 100644
--- a/.github/workflows/test-everything.yml
+++ b/.github/workflows/test-everything.yml
@@ -83,6 +83,7 @@ jobs:
             mpi:   "ON",
             simd:  "OFF"
           }
+        variant: [static, shared]
     env:
         CC:         ${{ matrix.config.cc }}
         CXX:        ${{ matrix.config.cxx }}
@@ -149,13 +150,22 @@ jobs:
           mpic++ --show
           mpicc --show
           echo $PYTHONPATH
-      - name: Build arbor
+      - if:   ${{ matrix.variant == 'static' }}
+        name: Build arbor
         run: |
           mkdir build
           cd build
           cmake .. -DCMAKE_CXX_COMPILER=$CXX -DCMAKE_C_COMPILER=$CC -DARB_WITH_PYTHON=ON -DARB_VECTORIZE=${{ matrix.config.simd }} -DPython3_EXECUTABLE=`which python` -DARB_WITH_MPI=${{ matrix.config.mpi }} -DARB_USE_BUNDLED_LIBS=ON -DARB_WITH_NEUROML=ON
           make -j4 tests examples pyarb html
           cd -
+      - if:   ${{ matrix.variant == 'shared' }}
+        name: Build arbor
+        run: |
+          mkdir build
+          cd build
+          cmake .. -DCMAKE_CXX_COMPILER=$CXX -DCMAKE_C_COMPILER=$CC -DARB_WITH_PYTHON=ON -DARB_VECTORIZE=${{ matrix.config.simd }} -DPython3_EXECUTABLE=`which python` -DARB_WITH_MPI=${{ matrix.config.mpi }} -DARB_USE_BUNDLED_LIBS=ON -DARB_WITH_NEUROML=ON -DBUILD_SHARED_LIBS=ON
+          make -j4 tests examples pyarb html
+          cd -
       - name: Install arbor
         run: |
           cd build
diff --git a/.gitignore b/.gitignore
index ee3b410fd70106ea214de3851302036bda6cda97..036ba49182d024c4de5daf45f4026bf7184de43d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,6 +24,7 @@ __pycache__
 *.swq
 *.swm
 *.swl
+*~
 
 .cache
 
diff --git a/CMakeLists.txt b/CMakeLists.txt
index d1a95db559e5f07afe811f3653df7f66c8d4a7ce..04a6c9b57c10cc3431a8ba583281f1b7ffd2820a 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -172,8 +172,11 @@ set(CMAKE_CXX_EXTENSIONS OFF)
 # Data and internal scripts go here
 set(ARB_INSTALL_DATADIR ${CMAKE_INSTALL_FULL_DATAROOTDIR}/arbor)
 # Derived paths for arbor-build-catalogue
-file(RELATIVE_PATH ARB_REL_DATADIR ${CMAKE_INSTALL_FULL_BINDIR} ${CMAKE_INSTALL_FULL_DATAROOTDIR}/arbor)
-file(RELATIVE_PATH ARB_REL_PACKAGEDIR ${CMAKE_INSTALL_FULL_BINDIR} ${CMAKE_INSTALL_FULL_LIBDIR}/cmake/arbor)
+get_filename_component(absolute_full_bindir ${CMAKE_INSTALL_FULL_BINDIR} REALPATH)
+get_filename_component(absolute_full_datarootdir ${CMAKE_INSTALL_FULL_DATAROOTDIR} REALPATH)
+get_filename_component(absolute_full_libdir ${CMAKE_INSTALL_FULL_LIBDIR} REALPATH)
+file(RELATIVE_PATH ARB_REL_DATADIR ${absolute_full_bindir} ${absolute_full_datarootdir}/arbor)
+file(RELATIVE_PATH ARB_REL_PACKAGEDIR ${absolute_full_bindir} ${absolute_full_libdir}/cmake/arbor)
 
 # Interface library `arbor-config-defs` collects configure-time defines
 # for arbor, arborenv, arborio, of the form ARB_HAVE_XXX. These
diff --git a/arbor/CMakeLists.txt b/arbor/CMakeLists.txt
index db81edbbd89510d908d677f7dbc5f4b6843a5b8f..7a1fb7a4fda8a9d9c22f55f24d5e5b0550047147 100644
--- a/arbor/CMakeLists.txt
+++ b/arbor/CMakeLists.txt
@@ -136,5 +136,10 @@ endif()
 
 set_target_properties(arbor PROPERTIES CUDA_RESOLVE_DEVICE_SYMBOLS ON)
 
+export_visibility(arbor)
+
+install(FILES ${CMAKE_CURRENT_BINARY_DIR}/include/arbor/export.hpp
+    DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/arbor)
+
 install(TARGETS arbor EXPORT arbor-targets ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR})
 
diff --git a/arbor/assert.cpp b/arbor/assert.cpp
index b17fa3251e1cbc423bf27aecbcd4639a184dc7c5..4205f641132add2c4e5b65e142dbc26d28012323 100644
--- a/arbor/assert.cpp
+++ b/arbor/assert.cpp
@@ -6,7 +6,7 @@
 
 namespace arb {
 
-void abort_on_failed_assertion(
+void ARB_ARBOR_API abort_on_failed_assertion(
     const char* assertion,
     const char* file,
     int line,
@@ -22,7 +22,7 @@ void abort_on_failed_assertion(
     std::abort();
 }
 
-void ignore_failed_assertion(
+void ARB_ARBOR_API ignore_failed_assertion(
     const char* assertion,
     const char* file,
     int line,
diff --git a/arbor/backends/gpu/forest.hpp b/arbor/backends/gpu/forest.hpp
index 1bc13bc858151a904b7b2c65ca5b1b55ff0343fe..7fc8be0cf94ea51b5044fedcdfe03aa91269cb46 100644
--- a/arbor/backends/gpu/forest.hpp
+++ b/arbor/backends/gpu/forest.hpp
@@ -2,6 +2,7 @@
 
 #include <vector>
 
+#include <arbor/export.hpp>
 #include "tree.hpp"
 
 namespace arb {
@@ -9,7 +10,7 @@ namespace gpu {
 
 using size_type = int;
 
-struct forest {
+struct ARB_ARBOR_API forest {
     forest(const std::vector<size_type>& p, const std::vector<size_type>& cell_cv_divs);
 
     void optimize();
diff --git a/arbor/backends/gpu/matrix_assemble.cu b/arbor/backends/gpu/matrix_assemble.cu
index 0f99eec5334cf05395c5e9bca07aa087ac18fd84..e1c385e4af930483bee6c0a9a9ad332192042334 100644
--- a/arbor/backends/gpu/matrix_assemble.cu
+++ b/arbor/backends/gpu/matrix_assemble.cu
@@ -154,7 +154,7 @@ void assemble_matrix_interleaved(
 
 } // namespace kernels
 
-void assemble_matrix_flat(
+ARB_ARBOR_API void assemble_matrix_flat(
         fvm_value_type* d,
         fvm_value_type* rhs,
         const fvm_value_type* invariant_d,
diff --git a/arbor/backends/gpu/matrix_fine.cu b/arbor/backends/gpu/matrix_fine.cu
index ca3592f15fd624a0678c56ffb358bd685af9bd38..675f6312c57977dde53594b591530872053eaf0c 100644
--- a/arbor/backends/gpu/matrix_fine.cu
+++ b/arbor/backends/gpu/matrix_fine.cu
@@ -245,7 +245,7 @@ void solve_matrix_fine(
 
 } // namespace kernels
 
-void gather(
+ARB_ARBOR_API void gather(
     const fvm_value_type* from,
     fvm_value_type* to,
     const fvm_index_type* p,
@@ -257,7 +257,7 @@ void gather(
     kernels::gather<<<griddim, blockdim>>>(from, to, p, n);
 }
 
-void scatter(
+ARB_ARBOR_API void scatter(
     const fvm_value_type* from,
     fvm_value_type* to,
     const fvm_index_type* p,
@@ -269,7 +269,7 @@ void scatter(
     kernels::scatter<<<griddim, blockdim>>>(from, to, p, n);
 }
 
-void assemble_matrix_fine(
+ARB_ARBOR_API void assemble_matrix_fine(
     fvm_value_type* d,
     fvm_value_type* rhs,
     const fvm_value_type* invariant_d,
@@ -308,7 +308,7 @@ void assemble_matrix_fine(
 // num_levels   = [3, 2, 3, ...]
 // num_cells    = [2, 3, ...]
 // num_blocks   = level_start.size() - 1 = num_levels.size() = num_cells.size()
-void solve_matrix_fine(
+ARB_ARBOR_API void solve_matrix_fine(
     fvm_value_type* rhs,
     fvm_value_type* d,                     // diagonal values
     const fvm_value_type* u,               // upper diagonal (and lower diagonal as the matrix is SPD)
diff --git a/arbor/backends/gpu/matrix_fine.hpp b/arbor/backends/gpu/matrix_fine.hpp
index 74b34906632a5047c6b4fe26b180166fd7895460..02c549039a591d2492202ce42cce702b25431ae7 100644
--- a/arbor/backends/gpu/matrix_fine.hpp
+++ b/arbor/backends/gpu/matrix_fine.hpp
@@ -1,5 +1,6 @@
 #include <arbor/fvm_types.hpp>
 
+#include <arbor/export.hpp>
 #include <ostream>
 
 namespace arb {
@@ -13,19 +14,19 @@ struct level_metadata {
 };
 
 // C wrappers around kernels
-void gather(
+ARB_ARBOR_API void gather(
     const fvm_value_type* from,
     fvm_value_type* to,
     const fvm_index_type* p,
     unsigned n);
 
-void scatter(
+ARB_ARBOR_API void scatter(
     const fvm_value_type* from,
     fvm_value_type* to,
     const fvm_index_type* p,
     unsigned n);
 
-void assemble_matrix_fine(
+ARB_ARBOR_API void assemble_matrix_fine(
     fvm_value_type* d,
     fvm_value_type* rhs,
     const fvm_value_type* invariant_d,
@@ -39,7 +40,7 @@ void assemble_matrix_fine(
     const fvm_index_type* perm,
     unsigned n);
 
-void solve_matrix_fine(
+ARB_ARBOR_API void solve_matrix_fine(
     fvm_value_type* rhs,
     fvm_value_type* d,                     // diagonal values
     const fvm_value_type* u,               // upper diagonal (and lower diagonal as the matrix is SPD)
diff --git a/arbor/backends/gpu/matrix_solve.cu b/arbor/backends/gpu/matrix_solve.cu
index 576f88e96f10b19bbe70ea61ec6fc1adfa6012bb..6a0a383ce0ab6791470a8da07935b86de40e1327 100644
--- a/arbor/backends/gpu/matrix_solve.cu
+++ b/arbor/backends/gpu/matrix_solve.cu
@@ -86,7 +86,7 @@ void solve_matrix_interleaved(
 
 } // namespace kernels
 
-void solve_matrix_flat(
+ARB_ARBOR_API void solve_matrix_flat(
     fvm_value_type* rhs,
     fvm_value_type* d,
     const fvm_value_type* u,
diff --git a/arbor/backends/gpu/matrix_state_flat.hpp b/arbor/backends/gpu/matrix_state_flat.hpp
index c5a6d98c6920b1165995682cb96aaa79b8ccb2d8..bc50b7fce3ea10aa7c8a1bccf319c58e4b42f3f1 100644
--- a/arbor/backends/gpu/matrix_state_flat.hpp
+++ b/arbor/backends/gpu/matrix_state_flat.hpp
@@ -1,5 +1,6 @@
 #pragma once
 
+#include <arbor/export.hpp>
 #include <arbor/fvm_types.hpp>
 
 #include "memory/memory.hpp"
@@ -13,7 +14,7 @@ namespace gpu {
 
 // CUDA implementation entry points:
 
-void solve_matrix_flat(
+ARB_ARBOR_API void solve_matrix_flat(
     fvm_value_type* rhs,
     fvm_value_type* d,
     const fvm_value_type* u,
@@ -21,7 +22,7 @@ void solve_matrix_flat(
     const fvm_index_type* cell_cv_divs,
     int num_mtx);
 
-void assemble_matrix_flat(
+ARB_ARBOR_API void assemble_matrix_flat(
     fvm_value_type* d,
     fvm_value_type* rhs,
     const fvm_value_type* invariant_d,
diff --git a/arbor/backends/gpu/multi_event_stream.hpp b/arbor/backends/gpu/multi_event_stream.hpp
index 929c325d0938859e786ec192e46d32b79d02ef30..83681db9fa41b920a488bea1b06a56e6e8d975b8 100644
--- a/arbor/backends/gpu/multi_event_stream.hpp
+++ b/arbor/backends/gpu/multi_event_stream.hpp
@@ -2,6 +2,7 @@
 
 // Indexed collection of pop-only event queues --- CUDA back-end implementation.
 
+#include <arbor/export.hpp>
 #include <arbor/arbexcept.hpp>
 #include <arbor/common_types.hpp>
 #include <arbor/fvm_types.hpp>
@@ -18,7 +19,7 @@ namespace arb {
 namespace gpu {
 
 // Base class provides common implementations across event types.
-class multi_event_stream_base {
+class ARB_ARBOR_API multi_event_stream_base {
 public:
     using size_type = cell_size_type;
     using value_type = fvm_value_type;
diff --git a/arbor/backends/gpu/shared_state.cpp b/arbor/backends/gpu/shared_state.cpp
index 44acd5e474cc27277022ee60a52424494a9a6e0f..bed738bc86b5d74e45105a932147621477d79ff7 100644
--- a/arbor/backends/gpu/shared_state.cpp
+++ b/arbor/backends/gpu/shared_state.cpp
@@ -459,7 +459,7 @@ void shared_state::take_samples(const sample_event_stream::state& s, array& samp
 }
 
 // Debug interface
-std::ostream& operator<<(std::ostream& o, shared_state& s) {
+ARB_ARBOR_API std::ostream& operator<<(std::ostream& o, shared_state& s) {
     o << " cv_to_intdom " << s.cv_to_intdom << "\n";
     o << " time         " << s.time << "\n";
     o << " time_to      " << s.time_to << "\n";
diff --git a/arbor/backends/gpu/shared_state.hpp b/arbor/backends/gpu/shared_state.hpp
index eb3301abcd8f2d6bd1456d8d04290209f07cc4e6..e0cacfc78d95acf1bf62d81d9d9cc01cf16d07a0 100644
--- a/arbor/backends/gpu/shared_state.hpp
+++ b/arbor/backends/gpu/shared_state.hpp
@@ -28,7 +28,7 @@ namespace gpu {
  *     Xo_     cao              external calcium concentration
  */
 
-struct ion_state {
+struct ARB_ARBOR_API ion_state {
     iarray node_index_; // Instance to CV map.
     array iX_;          // (A/m²) current density
     array eX_;          // (mV) reversal potential
@@ -62,7 +62,7 @@ struct ion_state {
     void reset();
 };
 
-struct istim_state {
+struct ARB_ARBOR_API istim_state {
     // Immutable data (post construction/initialization):
     iarray accu_index_;     // Instance to accumulator index (accu_stim_ index) map.
     iarray accu_to_cv_;     // Accumulator index to CV map.
@@ -99,7 +99,7 @@ struct istim_state {
     istim_state() = default;
 };
 
-struct shared_state {
+struct ARB_ARBOR_API shared_state {
     struct mech_storage {
         array data_;
         iarray indices_;
@@ -202,7 +202,7 @@ struct shared_state {
 };
 
 // For debugging only
-std::ostream& operator<<(std::ostream& o, shared_state& s);
+ARB_ARBOR_API std::ostream& operator<<(std::ostream& o, shared_state& s);
 
 } // namespace gpu
 } // namespace arb
diff --git a/arbor/backends/gpu/stimulus.cu b/arbor/backends/gpu/stimulus.cu
index ea7262958900575612a20783087b2c189a339f25..f6aa6036003110e133f091e679a774c0b0d7a43b 100644
--- a/arbor/backends/gpu/stimulus.cu
+++ b/arbor/backends/gpu/stimulus.cu
@@ -52,7 +52,7 @@ void istim_add_current_impl(int n, istim_pp pp) {
 
 } // namespace kernel
 
-void istim_add_current_impl(int n, const istim_pp& pp) {
+ARB_ARBOR_API void istim_add_current_impl(int n, const istim_pp& pp) {
     constexpr unsigned block_dim = 128;
     const unsigned grid_dim = impl::block_count(n, block_dim);
     if (!grid_dim) return;
diff --git a/arbor/backends/gpu/stimulus.hpp b/arbor/backends/gpu/stimulus.hpp
index 97447f6edef653aa3fa716694a62e0ac7eaa6238..736c0a160748ddf2bc2cbcc4ecc54366c7c2329f 100644
--- a/arbor/backends/gpu/stimulus.hpp
+++ b/arbor/backends/gpu/stimulus.hpp
@@ -1,5 +1,6 @@
 #pragma once
 
+#include <arbor/export.hpp>
 #include <arbor/fvm_types.hpp>
 
 namespace arb {
@@ -25,7 +26,7 @@ struct istim_pp {
     fvm_value_type* current_density;
 };
 
-void istim_add_current_impl(int n, const istim_pp& pp);
+ARB_ARBOR_API void istim_add_current_impl(int n, const istim_pp& pp);
 
 } // namespace gpu
 } // namespace arb
diff --git a/arbor/backends/multicore/shared_state.cpp b/arbor/backends/multicore/shared_state.cpp
index b282186f16657b9ded74a292f6f546e19312a33a..538f7718d56db69592d5dc0bd76b86b42a7f12e6 100644
--- a/arbor/backends/multicore/shared_state.cpp
+++ b/arbor/backends/multicore/shared_state.cpp
@@ -347,7 +347,7 @@ void shared_state::take_samples(
 }
 
 // (Debug interface only.)
-std::ostream& operator<<(std::ostream& out, const shared_state& s) {
+ARB_ARBOR_API std::ostream& operator<<(std::ostream& out, const shared_state& s) {
     using io::csv;
 
     out << "n_intdom     " << s.n_intdom << "\n";
diff --git a/arbor/backends/multicore/shared_state.hpp b/arbor/backends/multicore/shared_state.hpp
index 4d12e0f414d5be33dec4bd26d3a2687a633aa763..ab9fa7f5342bd463b5ec015dc695b73f641f4aab 100644
--- a/arbor/backends/multicore/shared_state.hpp
+++ b/arbor/backends/multicore/shared_state.hpp
@@ -7,6 +7,7 @@
 #include <utility>
 #include <vector>
 
+#include <arbor/export.hpp>
 #include <arbor/assert.hpp>
 #include <arbor/common_types.hpp>
 #include <arbor/fvm_types.hpp>
@@ -38,7 +39,7 @@ namespace multicore {
  *     Xo_     cao              external calcium concentration
  */
 
-struct ion_state {
+struct ARB_ARBOR_API ion_state {
     unsigned alignment = 1; // Alignment and padding multiple.
 
     iarray node_index_;     // Instance to CV map.
@@ -73,7 +74,7 @@ struct ion_state {
     void reset();
 };
 
-struct istim_state {
+struct ARB_ARBOR_API istim_state {
     unsigned alignment = 1; // Alignment and padding multiple.
 
     // Immutable data (post initialization):
@@ -105,7 +106,7 @@ struct istim_state {
     istim_state() = default;
 };
 
-struct shared_state {
+struct ARB_ARBOR_API shared_state {
     struct mech_storage {
         array data_;
         iarray indices_;
@@ -207,7 +208,7 @@ struct shared_state {
 };
 
 // For debugging only:
-std::ostream& operator<<(std::ostream& o, const shared_state& s);
+ARB_ARBOR_API std::ostream& operator<<(std::ostream& o, const shared_state& s);
 
 
 } // namespace multicore
diff --git a/arbor/cable_cell_param.cpp b/arbor/cable_cell_param.cpp
index c238a48e57ee927133de5606f07d911614faee4d..36b17d9d70962d8baba60b65c7765f754d67d6f5 100644
--- a/arbor/cable_cell_param.cpp
+++ b/arbor/cable_cell_param.cpp
@@ -12,7 +12,7 @@
 
 namespace arb {
 
-void check_global_properties(const cable_cell_global_properties& G) {
+ARB_ARBOR_API void check_global_properties(const cable_cell_global_properties& G) {
     auto& param = G.default_parameters;
 
     if (!param.init_membrane_potential) {
diff --git a/arbor/cell_group_factory.cpp b/arbor/cell_group_factory.cpp
index 1351299fc8dfc5d1c3bfe20e0c915081b378dae9..69af0ffdcba1ef98c2ba8f8edd64ce27e2ed7cce 100644
--- a/arbor/cell_group_factory.cpp
+++ b/arbor/cell_group_factory.cpp
@@ -19,7 +19,7 @@ cell_group_ptr make_cell_group(Args&&... args) {
     return cell_group_ptr(new Impl(std::forward<Args>(args)...));
 }
 
-cell_group_factory cell_kind_implementation(
+ARB_ARBOR_API cell_group_factory cell_kind_implementation(
         cell_kind ck, backend_kind bk, const execution_context& ctx)
 {
     using gid_vector = std::vector<cell_gid_type>;
diff --git a/arbor/cell_group_factory.hpp b/arbor/cell_group_factory.hpp
index 7723d822b4af4f8978af8f4d4737deb3da40b720..b8a321c423c53891673bd7900c47bc8702e00761 100644
--- a/arbor/cell_group_factory.hpp
+++ b/arbor/cell_group_factory.hpp
@@ -10,6 +10,7 @@
 #include <vector>
 
 #include <arbor/common_types.hpp>
+#include <arbor/export.hpp>
 #include <arbor/recipe.hpp>
 
 #include "cell_group.hpp"
@@ -20,7 +21,7 @@ namespace arb {
 using cell_group_factory = std::function<
         cell_group_ptr(const std::vector<cell_gid_type>&, const recipe&, cell_label_range& cg_sources, cell_label_range& cg_targets)>;
 
-cell_group_factory cell_kind_implementation(
+ARB_ARBOR_API cell_group_factory cell_kind_implementation(
         cell_kind, backend_kind, const execution_context&);
 
 inline bool cell_kind_supported(
diff --git a/arbor/common_types_io.cpp b/arbor/common_types_io.cpp
index 366ba114f8ab323ce74e029a12fffa7adb4ac331..81cbc05d35cce691bfdbefd00fe3797a59fd1539 100644
--- a/arbor/common_types_io.cpp
+++ b/arbor/common_types_io.cpp
@@ -4,7 +4,7 @@
 
 namespace arb {
 
-std::ostream& operator<<(std::ostream& o, lid_selection_policy policy) {
+ARB_ARBOR_API std::ostream& operator<<(std::ostream& o, lid_selection_policy policy) {
     switch (policy) {
     case lid_selection_policy::round_robin:
         return o << "round_robin";
@@ -14,11 +14,11 @@ std::ostream& operator<<(std::ostream& o, lid_selection_policy policy) {
     return o;
 }
 
-std::ostream& operator<<(std::ostream& o, arb::cell_member_type m) {
+ARB_ARBOR_API std::ostream& operator<<(std::ostream& o, arb::cell_member_type m) {
     return o << m.gid << ':' << m.index;
 }
 
-std::ostream& operator<<(std::ostream& o, arb::cell_kind k) {
+ARB_ARBOR_API std::ostream& operator<<(std::ostream& o, arb::cell_kind k) {
     o << "cell_kind::";
     switch (k) {
     case arb::cell_kind::spike_source:
@@ -33,7 +33,7 @@ std::ostream& operator<<(std::ostream& o, arb::cell_kind k) {
     return o;
 }
 
-std::ostream& operator<<(std::ostream& o, arb::backend_kind k) {
+ARB_ARBOR_API std::ostream& operator<<(std::ostream& o, arb::backend_kind k) {
     o << "backend_kind::";
     switch (k) {
     case arb::backend_kind::multicore:
diff --git a/arbor/communication/communicator.hpp b/arbor/communication/communicator.hpp
index 33e8f2f743971e083c3a9d2f6875121f2e54c093..fa93adfc2fde1a763476588333fb36572700a9c9 100644
--- a/arbor/communication/communicator.hpp
+++ b/arbor/communication/communicator.hpp
@@ -2,6 +2,7 @@
 
 #include <vector>
 
+#include <arbor/export.hpp>
 #include <arbor/common_types.hpp>
 #include <arbor/domain_decomposition.hpp>
 #include <arbor/recipe.hpp>
@@ -25,7 +26,7 @@ namespace arb {
 // to build the data structures required for efficient spike communication and
 // event generation.
 
-class communicator {
+class ARB_ARBOR_API communicator {
 public:
     communicator() {}
 
diff --git a/arbor/communication/dry_run_context.cpp b/arbor/communication/dry_run_context.cpp
index e3a12d28c144258d41995446d28dee5a99f4454b..fcfe4f9f568f9a7d52e71e08dff1e2f3b92cb9e6 100644
--- a/arbor/communication/dry_run_context.cpp
+++ b/arbor/communication/dry_run_context.cpp
@@ -130,7 +130,7 @@ struct dry_run_context_impl {
     unsigned num_cells_per_tile_;
 };
 
-std::shared_ptr<distributed_context> make_dry_run_context(unsigned num_ranks, unsigned num_cells_per_tile) {
+ARB_ARBOR_API std::shared_ptr<distributed_context> make_dry_run_context(unsigned num_ranks, unsigned num_cells_per_tile) {
     return std::make_shared<distributed_context>(dry_run_context_impl(num_ranks, num_cells_per_tile));
 }
 
diff --git a/arbor/communication/mpi.cpp b/arbor/communication/mpi.cpp
index 92619e050479874b8ef3e85ed5fd408d009f6529..1a8a9111889046138db520a270f0e69d944e899c 100644
--- a/arbor/communication/mpi.cpp
+++ b/arbor/communication/mpi.cpp
@@ -5,19 +5,19 @@
 namespace arb {
 namespace mpi {
 
-int rank(MPI_Comm comm) {
+ARB_ARBOR_API int rank(MPI_Comm comm) {
     int r;
     MPI_OR_THROW(MPI_Comm_rank, comm, &r);
     return r;
 }
 
-int size(MPI_Comm comm) {
+ARB_ARBOR_API int size(MPI_Comm comm) {
     int s;
     MPI_OR_THROW(MPI_Comm_size, comm, &s);
     return s;
 }
 
-void barrier(MPI_Comm comm) {
+ARB_ARBOR_API void barrier(MPI_Comm comm) {
     MPI_OR_THROW(MPI_Barrier, comm);
 }
 
diff --git a/arbor/communication/mpi.hpp b/arbor/communication/mpi.hpp
index 7a3546ff1fc78ae8d0eed95137d6791ba149ee64..df3eaecb85e0453ed967a38eaf227698cf9998c8 100644
--- a/arbor/communication/mpi.hpp
+++ b/arbor/communication/mpi.hpp
@@ -7,6 +7,7 @@
 
 #include <mpi.h>
 
+#include <arbor/export.hpp>
 #include <arbor/assert.hpp>
 #include <arbor/communication/mpi_error.hpp>
 
@@ -19,9 +20,9 @@ namespace arb {
 namespace mpi {
 
 // prototypes
-int rank(MPI_Comm);
-int size(MPI_Comm);
-void barrier(MPI_Comm);
+ARB_ARBOR_API int rank(MPI_Comm);
+ARB_ARBOR_API int size(MPI_Comm);
+ARB_ARBOR_API void barrier(MPI_Comm);
 
 #define MPI_OR_THROW(fn, ...)\
 while (int r_ = fn(__VA_ARGS__)) throw mpi_error(r_, #fn)
diff --git a/arbor/communication/mpi_error.cpp b/arbor/communication/mpi_error.cpp
index de511c216673245531e696b7cf6bf39090f549d1..78ffed257f23efd53efa0eac6c5c2339add0d94d 100644
--- a/arbor/communication/mpi_error.cpp
+++ b/arbor/communication/mpi_error.cpp
@@ -4,7 +4,7 @@
 
 namespace arb {
 
-const mpi_error_category_impl& mpi_error_category() {
+ARB_ARBOR_API const mpi_error_category_impl& mpi_error_category() {
     static mpi_error_category_impl the_category;
     return the_category;
 }
diff --git a/arbor/cv_policy.cpp b/arbor/cv_policy.cpp
index 11135cf7b16d1f41f7e6d827db61b1f64edbb2fb..b2962c7fa4f51ed0d9503bbf3af8f95b8b6bd18f 100644
--- a/arbor/cv_policy.cpp
+++ b/arbor/cv_policy.cpp
@@ -44,7 +44,7 @@ struct cv_policy_plus_: cv_policy_base {
     cv_policy lhs_, rhs_;
 };
 
-cv_policy operator+(const cv_policy& lhs, const cv_policy& rhs) {
+ARB_ARBOR_API cv_policy operator+(const cv_policy& lhs, const cv_policy& rhs) {
     return cv_policy_plus_(lhs, rhs);
 }
 
@@ -70,7 +70,7 @@ struct cv_policy_bar_: cv_policy_base {
     cv_policy lhs_, rhs_;
 };
 
-cv_policy operator|(const cv_policy& lhs, const cv_policy& rhs) {
+ARB_ARBOR_API cv_policy operator|(const cv_policy& lhs, const cv_policy& rhs) {
     return cv_policy_bar_(lhs, rhs);
 }
 
diff --git a/arbor/distributed_context.hpp b/arbor/distributed_context.hpp
index 2cea04e58fcf79c09c2360ba9c0d9310c26c7c2e..58ca11c318e3c754c32e9aacc4aba7949e268484 100644
--- a/arbor/distributed_context.hpp
+++ b/arbor/distributed_context.hpp
@@ -3,6 +3,7 @@
 #include <memory>
 #include <string>
 
+#include <arbor/export.hpp>
 #include <arbor/spike.hpp>
 #include <arbor/util/pp_util.hpp>
 
@@ -236,7 +237,7 @@ distributed_context_handle make_local_context() {
     return std::make_shared<distributed_context>();
 }
 
-distributed_context_handle make_dry_run_context(unsigned num_ranks, unsigned num_cells_per_rank);
+ARB_ARBOR_API distributed_context_handle make_dry_run_context(unsigned num_ranks, unsigned num_cells_per_rank);
 
 // MPI context creation functions only provided if built with MPI support.
 template <typename MPICommType>
diff --git a/arbor/event_binner.hpp b/arbor/event_binner.hpp
index 7d12526457dc0365d57f7a93196a376ca4581938..fbec4cb5c4bb14bbca6ba9851f5e19902dbd1b83 100644
--- a/arbor/event_binner.hpp
+++ b/arbor/event_binner.hpp
@@ -4,12 +4,13 @@
 #include <optional>
 #include <unordered_map>
 
+#include <arbor/export.hpp>
 #include <arbor/common_types.hpp>
 #include <arbor/spike.hpp>
 
 namespace arb {
 
-class event_binner {
+class ARB_ARBOR_API event_binner {
 public:
     event_binner(): policy_(binning_kind::none), bin_interval_(0) {}
 
diff --git a/arbor/execution_context.cpp b/arbor/execution_context.cpp
index 3e4cd629c35246d9072e682d56d02b95cb808f4c..0437024b7b759d336a097ed79773570bc9122c69 100644
--- a/arbor/execution_context.cpp
+++ b/arbor/execution_context.cpp
@@ -25,7 +25,7 @@ execution_context::execution_context(const proc_allocation& resources):
                            : std::make_shared<gpu_context>())
 {}
 
-context make_context(const proc_allocation& p) {
+ARB_ARBOR_API context make_context(const proc_allocation& p) {
     return context(new execution_context(p));
 }
 
@@ -39,7 +39,7 @@ execution_context::execution_context(const proc_allocation& resources, MPI_Comm
 {}
 
 template <>
-context make_context<MPI_Comm>(const proc_allocation& p, MPI_Comm comm) {
+ARB_ARBOR_API context make_context<MPI_Comm>(const proc_allocation& p, MPI_Comm comm) {
     return context(new execution_context(p, comm));
 }
 #endif
@@ -54,31 +54,31 @@ execution_context::execution_context(
 {}
 
 template <>
-context make_context(const proc_allocation& p, dry_run_info d) {
+ARB_ARBOR_API context make_context(const proc_allocation& p, dry_run_info d) {
     return context(new execution_context(p, d));
 }
 
-std::string distribution_type(const context& ctx) {
+ARB_ARBOR_API std::string distribution_type(const context& ctx) {
     return ctx->distributed->name();
 }
 
-bool has_gpu(const context& ctx) {
+ARB_ARBOR_API bool has_gpu(const context& ctx) {
     return ctx->gpu->has_gpu();
 }
 
-unsigned num_threads(const context& ctx) {
+ARB_ARBOR_API unsigned num_threads(const context& ctx) {
     return ctx->thread_pool->get_num_threads();
 }
 
-unsigned num_ranks(const context& ctx) {
+ARB_ARBOR_API unsigned num_ranks(const context& ctx) {
     return ctx->distributed->size();
 }
 
-unsigned rank(const context& ctx) {
+ARB_ARBOR_API unsigned rank(const context& ctx) {
     return ctx->distributed->id();
 }
 
-bool has_mpi(const context& ctx) {
+ARB_ARBOR_API bool has_mpi(const context& ctx) {
     return ctx->distributed->name() == "MPI";
 }
 
diff --git a/arbor/execution_context.hpp b/arbor/execution_context.hpp
index 79935f75fae75ecfe7ce5abf3cc55c405707d757..0035e39d69d692aca423092fa4b768e30fe7a236 100644
--- a/arbor/execution_context.hpp
+++ b/arbor/execution_context.hpp
@@ -2,6 +2,7 @@
 
 #include <memory>
 
+#include <arbor/export.hpp>
 #include <arbor/context.hpp>
 
 #include "distributed_context.hpp"
@@ -19,7 +20,7 @@ namespace arb {
 // execution_context, to hide implementation details of the
 // container and its constituent contexts from the public API.
 
-struct execution_context {
+struct ARB_ARBOR_API execution_context {
     distributed_context_handle distributed;
     task_system_handle thread_pool;
     gpu_context_handle gpu;
diff --git a/arbor/fvm_layout.cpp b/arbor/fvm_layout.cpp
index f574615c67e2e0f7bb2d5b9e1bccf0383cd284b7..f231defdabf62d9678dba21a9730f34e566fc3f8 100644
--- a/arbor/fvm_layout.cpp
+++ b/arbor/fvm_layout.cpp
@@ -170,7 +170,7 @@ namespace impl {
 
 // Merge CV geometry lists in-place.
 
-cv_geometry& append(cv_geometry& geom, const cv_geometry& right) {
+ARB_ARBOR_API cv_geometry& append(cv_geometry& geom, const cv_geometry& right) {
     using util::append;
     using impl::tail;
     using impl::append_offset;
@@ -204,7 +204,7 @@ cv_geometry& append(cv_geometry& geom, const cv_geometry& right) {
 
 // Combine two fvm_cv_geometry groups in-place.
 
-fvm_cv_discretization& append(fvm_cv_discretization& dczn, const fvm_cv_discretization& right) {
+ARB_ARBOR_API fvm_cv_discretization& append(fvm_cv_discretization& dczn, const fvm_cv_discretization& right) {
     using util::append;
 
     append(dczn.geometry, right.geometry);
@@ -224,7 +224,7 @@ fvm_cv_discretization& append(fvm_cv_discretization& dczn, const fvm_cv_discreti
 // FVM discretization
 // ------------------
 
-fvm_cv_discretization fvm_cv_discretize(const cable_cell& cell, const cable_cell_parameter_set& global_dflt) {
+ARB_ARBOR_API fvm_cv_discretization fvm_cv_discretize(const cable_cell& cell, const cable_cell_parameter_set& global_dflt) {
     const auto& dflt = cell.default_parameters();
     fvm_cv_discretization D;
 
@@ -345,7 +345,7 @@ fvm_cv_discretization fvm_cv_discretize(const cable_cell& cell, const cable_cell
     return D;
 }
 
-fvm_cv_discretization fvm_cv_discretize(const std::vector<cable_cell>& cells,
+ARB_ARBOR_API fvm_cv_discretization fvm_cv_discretize(const std::vector<cable_cell>& cells,
     const cable_cell_parameter_set& global_defaults,
     const arb::execution_context& ctx)
 {
@@ -515,7 +515,7 @@ voltage_reference_pair fvm_voltage_reference_points(const morphology& morph, con
 
 // Interpolate membrane voltage from reference points in adjacent CVs.
 
-fvm_voltage_interpolant fvm_interpolate_voltage(const cable_cell& cell, const fvm_cv_discretization& D, fvm_size_type cell_idx, mlocation site) {
+ARB_ARBOR_API fvm_voltage_interpolant fvm_interpolate_voltage(const cable_cell& cell, const fvm_cv_discretization& D, fvm_size_type cell_idx, mlocation site) {
     auto& embedding = cell.embedding();
     fvm_voltage_interpolant vi;
 
@@ -556,7 +556,7 @@ fvm_voltage_interpolant fvm_interpolate_voltage(const cable_cell& cell, const fv
 
 // Axial current as linear combination of membrane voltages at reference points in adjacent CVs.
 
-fvm_voltage_interpolant fvm_axial_current(const cable_cell& cell, const fvm_cv_discretization& D, fvm_size_type cell_idx, mlocation site) {
+ARB_ARBOR_API fvm_voltage_interpolant fvm_axial_current(const cable_cell& cell, const fvm_cv_discretization& D, fvm_size_type cell_idx, mlocation site) {
     auto& embedding = cell.embedding();
     fvm_voltage_interpolant vi;
 
@@ -653,7 +653,7 @@ fvm_mechanism_data& append(fvm_mechanism_data& left, const fvm_mechanism_data& r
     return left;
 }
 
-std::unordered_map<cell_member_type, fvm_size_type> fvm_build_gap_junction_cv_map(
+ARB_ARBOR_API std::unordered_map<cell_member_type, fvm_size_type> fvm_build_gap_junction_cv_map(
     const std::vector<cable_cell>& cells,
     const std::vector<cell_gid_type>& gids,
     const fvm_cv_discretization& D)
@@ -670,7 +670,7 @@ std::unordered_map<cell_member_type, fvm_size_type> fvm_build_gap_junction_cv_ma
     return gj_cvs;
 }
 
-std::unordered_map<cell_gid_type, std::vector<fvm_gap_junction>> fvm_resolve_gj_connections(
+ARB_ARBOR_API std::unordered_map<cell_gid_type, std::vector<fvm_gap_junction>> fvm_resolve_gj_connections(
     const std::vector<cell_gid_type>& gids,
     const cell_label_range& gj_data,
     const std::unordered_map<cell_member_type, fvm_size_type>& gj_cvs,
@@ -705,7 +705,7 @@ fvm_mechanism_data fvm_build_mechanism_data(
     const fvm_cv_discretization& D,
     fvm_size_type cell_idx);
 
-fvm_mechanism_data fvm_build_mechanism_data(
+ARB_ARBOR_API fvm_mechanism_data fvm_build_mechanism_data(
     const cable_cell_global_properties& gprop,
     const std::vector<cable_cell>& cells,
     const std::vector<cell_gid_type>& gids,
diff --git a/arbor/fvm_layout.hpp b/arbor/fvm_layout.hpp
index 7d178dc6112fc04df372fbc4669ea791200c37c3..1ebe0827aa4dd06d0e5cfb043055b1a7075abbd4 100644
--- a/arbor/fvm_layout.hpp
+++ b/arbor/fvm_layout.hpp
@@ -4,6 +4,7 @@
 #include <utility>
 #include <vector>
 
+#include <arbor/export.hpp>
 #include <arbor/cable_cell.hpp>
 #include <arbor/mechanism.hpp>
 #include <arbor/mechinfo.hpp>
@@ -55,7 +56,7 @@ namespace cv_prefer {
     };
 }
 
-struct cv_geometry: public cell_cv_data_impl {
+struct ARB_ARBOR_API cv_geometry: public cell_cv_data_impl {
     using base = cell_cv_data_impl;
 
     using size_type = fvm_size_type;
@@ -119,7 +120,7 @@ struct cv_geometry: public cell_cv_data_impl {
 
 // Combine two cv_geometry groups in-place.
 // (Returns reference to first argument.)
-cv_geometry& append(cv_geometry&, const cv_geometry&);
+ARB_ARBOR_API cv_geometry& append(cv_geometry&, const cv_geometry&);
 
 // Discretization of morphologies and physical properties. Contains cv_geometry
 // as above.
@@ -161,11 +162,11 @@ struct fvm_cv_discretization {
 
 // Combine two fvm_cv_geometry groups in-place.
 // (Returns reference to first argument.)
-fvm_cv_discretization& append(fvm_cv_discretization&, const fvm_cv_discretization&);
+ARB_ARBOR_API fvm_cv_discretization& append(fvm_cv_discretization&, const fvm_cv_discretization&);
 
 // Construct fvm_cv_discretization from one or more cells.
-fvm_cv_discretization fvm_cv_discretize(const cable_cell& cell, const cable_cell_parameter_set& global_dflt);
-fvm_cv_discretization fvm_cv_discretize(const std::vector<cable_cell>& cells, const cable_cell_parameter_set& global_defaults, const arb::execution_context& ctx={});
+ARB_ARBOR_API fvm_cv_discretization fvm_cv_discretize(const cable_cell& cell, const cable_cell_parameter_set& global_dflt);
+ARB_ARBOR_API fvm_cv_discretization fvm_cv_discretize(const std::vector<cable_cell>& cells, const cable_cell_parameter_set& global_defaults, const arb::execution_context& ctx={});
 
 
 // Interpolant data for voltage, axial current probes.
@@ -179,10 +180,10 @@ struct fvm_voltage_interpolant {
 };
 
 // Interpolated membrane voltage.
-fvm_voltage_interpolant fvm_interpolate_voltage(const cable_cell& cell, const fvm_cv_discretization& D, fvm_size_type cell_idx, mlocation site);
+ARB_ARBOR_API fvm_voltage_interpolant fvm_interpolate_voltage(const cable_cell& cell, const fvm_cv_discretization& D, fvm_size_type cell_idx, mlocation site);
 
 // Axial current as linear combiantion of voltages.
-fvm_voltage_interpolant fvm_axial_current(const cable_cell& cell, const fvm_cv_discretization& D, fvm_size_type cell_idx, mlocation site);
+ARB_ARBOR_API fvm_voltage_interpolant fvm_axial_current(const cable_cell& cell, const fvm_cv_discretization& D, fvm_size_type cell_idx, mlocation site);
 
 
 // Post-discretization data for point and density mechanism instantiation.
@@ -257,13 +258,13 @@ struct fvm_stimulus_config {
 };
 
 // Maps gj {gid, lid} locations on a cell to their CV indices.
-std::unordered_map<cell_member_type, fvm_size_type> fvm_build_gap_junction_cv_map(
+ARB_ARBOR_API std::unordered_map<cell_member_type, fvm_size_type> fvm_build_gap_junction_cv_map(
     const std::vector<cable_cell>& cells,
     const std::vector<cell_gid_type>& gids,
     const fvm_cv_discretization& D);
 
 // Resolves gj_connections into {gid, lid} pairs, then to CV indices and a weight.
-std::unordered_map<cell_gid_type, std::vector<fvm_gap_junction>> fvm_resolve_gj_connections(
+ARB_ARBOR_API std::unordered_map<cell_gid_type, std::vector<fvm_gap_junction>> fvm_resolve_gj_connections(
     const std::vector<cell_gid_type>& gids,
     const cell_label_range& gj_data,
     const std::unordered_map<cell_member_type, fvm_size_type>& gj_cv,
@@ -289,7 +290,7 @@ struct fvm_mechanism_data {
     bool post_events = false;
 };
 
-fvm_mechanism_data fvm_build_mechanism_data(
+ARB_ARBOR_API fvm_mechanism_data fvm_build_mechanism_data(
     const cable_cell_global_properties& gprop,
     const std::vector<cable_cell>& cells,
     const std::vector<cell_gid_type>& gids,
diff --git a/arbor/fvm_lowered_cell.hpp b/arbor/fvm_lowered_cell.hpp
index 6aec640a426f8a956f1b051a6cf4c2393f7fdf9b..a3bff57d86d425d91ada0a000d26fc3d61f64a7d 100644
--- a/arbor/fvm_lowered_cell.hpp
+++ b/arbor/fvm_lowered_cell.hpp
@@ -7,6 +7,7 @@
 #include <variant>
 #include <vector>
 
+#include <arbor/export.hpp>
 #include <arbor/assert.hpp>
 #include <arbor/common_types.hpp>
 #include <arbor/cable_cell.hpp>
@@ -235,6 +236,6 @@ struct fvm_lowered_cell {
 
 using fvm_lowered_cell_ptr = std::unique_ptr<fvm_lowered_cell>;
 
-fvm_lowered_cell_ptr make_fvm_lowered_cell(backend_kind p, const execution_context& ctx);
+ARB_ARBOR_API fvm_lowered_cell_ptr make_fvm_lowered_cell(backend_kind p, const execution_context& ctx);
 
 } // namespace arb
diff --git a/arbor/gpu_context.cpp b/arbor/gpu_context.cpp
index 273c00585198604abcdb11538f54ff46f29261d1..baad4acfea3028fc96593d87738cdae4cb65d245 100644
--- a/arbor/gpu_context.cpp
+++ b/arbor/gpu_context.cpp
@@ -14,7 +14,7 @@ enum gpu_flags {
     has_atomic_double = 1
 };
 
-gpu_context_handle make_gpu_context(int id) {
+ARB_ARBOR_API gpu_context_handle make_gpu_context(int id) {
     return std::make_shared<gpu_context>(id);
 }
 
diff --git a/arbor/gpu_context.hpp b/arbor/gpu_context.hpp
index d394e4d2066b04d158347a9b39c3ee59b488331a..6d8965558b212ccfae13067f2749eca01ce17d00 100644
--- a/arbor/gpu_context.hpp
+++ b/arbor/gpu_context.hpp
@@ -3,9 +3,11 @@
 #include <cstdlib>
 #include <memory>
 
+#include <arbor/export.hpp>
+
 namespace arb {
 
-class gpu_context {
+class ARB_ARBOR_API gpu_context {
     int id_ = -1;
     std::size_t attributes_ = 0;
 
@@ -21,6 +23,6 @@ public:
 };
 
 using gpu_context_handle = std::shared_ptr<gpu_context>;
-gpu_context_handle make_gpu_context(int id);
+ARB_ARBOR_API gpu_context_handle make_gpu_context(int id);
 
 } // namespace arb
diff --git a/arbor/include/arbor/arbexcept.hpp b/arbor/include/arbor/arbexcept.hpp
index 2574c71febadec29959f5ac51b063333edb9159e..dee6b57e990d48ddd95afc23ba26ca33ecc5cdab 100644
--- a/arbor/include/arbor/arbexcept.hpp
+++ b/arbor/include/arbor/arbexcept.hpp
@@ -5,6 +5,7 @@
 #include <string>
 
 #include <arbor/common_types.hpp>
+#include <arbor/export.hpp>
 
 // Arbor-specific exception hierarchy.
 
@@ -13,7 +14,7 @@ namespace arb {
 // Arbor internal logic error (if these are thrown,
 // there is a bug in the library.)
 
-struct arbor_internal_error: std::logic_error {
+struct ARB_SYMBOL_VISIBLE arbor_internal_error: std::logic_error {
     arbor_internal_error(const std::string& what_arg):
         std::logic_error(what_arg)
     {}
@@ -22,7 +23,7 @@ struct arbor_internal_error: std::logic_error {
 
 // Common base-class for arbor run-time errors.
 
-struct arbor_exception: std::runtime_error {
+struct ARB_SYMBOL_VISIBLE arbor_exception: std::runtime_error {
     arbor_exception(const std::string& what_arg):
         std::runtime_error(what_arg)
     {}
@@ -31,52 +32,52 @@ struct arbor_exception: std::runtime_error {
 // Logic errors
 
 // Argument violates domain constraints, eg ln(-1)
-struct domain_error: arbor_exception {
+struct ARB_SYMBOL_VISIBLE domain_error: arbor_exception {
     domain_error(const std::string&);
 };
 
 // Recipe errors:
 
-struct bad_cell_probe: arbor_exception {
+struct ARB_SYMBOL_VISIBLE bad_cell_probe: arbor_exception {
     bad_cell_probe(cell_kind kind, cell_gid_type gid);
     cell_gid_type gid;
     cell_kind kind;
 };
 
-struct bad_cell_description: arbor_exception {
+struct ARB_SYMBOL_VISIBLE bad_cell_description: arbor_exception {
     bad_cell_description(cell_kind kind, cell_gid_type gid);
     cell_gid_type gid;
     cell_kind kind;
 };
 
-struct bad_connection_source_gid: arbor_exception {
+struct ARB_SYMBOL_VISIBLE bad_connection_source_gid: arbor_exception {
     bad_connection_source_gid(cell_gid_type gid, cell_gid_type src_gid, cell_size_type num_cells);
     cell_gid_type gid, src_gid;
     cell_size_type num_cells;
 };
 
-struct bad_connection_label: arbor_exception {
+struct ARB_SYMBOL_VISIBLE bad_connection_label: arbor_exception {
     bad_connection_label(cell_gid_type gid, const cell_tag_type& label, const std::string& msg);
     cell_gid_type gid;
     cell_tag_type label;
 };
 
-struct bad_global_property: arbor_exception {
+struct ARB_SYMBOL_VISIBLE bad_global_property: arbor_exception {
     explicit bad_global_property(cell_kind kind);
     cell_kind kind;
 };
 
-struct bad_probe_id: arbor_exception {
+struct ARB_SYMBOL_VISIBLE bad_probe_id: arbor_exception {
     explicit bad_probe_id(cell_member_type id);
     cell_member_type probe_id;
 };
 
-struct gj_kind_mismatch: arbor_exception {
+struct ARB_SYMBOL_VISIBLE gj_kind_mismatch: arbor_exception {
     gj_kind_mismatch(cell_gid_type gid_0, cell_gid_type gid_1);
     cell_gid_type gid_0, gid_1;
 };
 
-struct gj_unsupported_lid_selection_policy: arbor_exception {
+struct ARB_SYMBOL_VISIBLE gj_unsupported_lid_selection_policy: arbor_exception {
     gj_unsupported_lid_selection_policy(cell_gid_type gid, cell_tag_type label);
     cell_gid_type gid;
     cell_tag_type label;
@@ -84,21 +85,21 @@ struct gj_unsupported_lid_selection_policy: arbor_exception {
 
 // Context errors:
 
-struct zero_thread_requested_error: arbor_exception {
+struct ARB_SYMBOL_VISIBLE zero_thread_requested_error: arbor_exception {
     zero_thread_requested_error(unsigned nbt);
     unsigned nbt;
 };
 
 // Domain decomposition errors:
 
-struct gj_unsupported_domain_decomposition: arbor_exception {
+struct ARB_SYMBOL_VISIBLE gj_unsupported_domain_decomposition: arbor_exception {
     gj_unsupported_domain_decomposition(cell_gid_type gid_0, cell_gid_type gid_1);
     cell_gid_type gid_0, gid_1;
 };
 
 // Simulation errors:
 
-struct bad_event_time: arbor_exception {
+struct ARB_SYMBOL_VISIBLE bad_event_time: arbor_exception {
     explicit bad_event_time(time_type event_time, time_type sim_time);
     time_type event_time;
     time_type sim_time;
@@ -106,28 +107,28 @@ struct bad_event_time: arbor_exception {
 
 // Mechanism catalogue errors:
 
-struct no_such_mechanism: arbor_exception {
+struct ARB_SYMBOL_VISIBLE no_such_mechanism: arbor_exception {
     explicit no_such_mechanism(const std::string& mech_name);
     std::string mech_name;
 };
 
-struct duplicate_mechanism: arbor_exception {
+struct ARB_SYMBOL_VISIBLE duplicate_mechanism: arbor_exception {
     explicit duplicate_mechanism(const std::string& mech_name);
     std::string mech_name;
 };
 
-struct fingerprint_mismatch: arbor_exception {
+struct ARB_SYMBOL_VISIBLE fingerprint_mismatch: arbor_exception {
     explicit fingerprint_mismatch(const std::string& mech_name);
     std::string mech_name;
 };
 
-struct no_such_parameter: arbor_exception {
+struct ARB_SYMBOL_VISIBLE no_such_parameter: arbor_exception {
     no_such_parameter(const std::string& mech_name, const std::string& param_name);
     std::string mech_name;
     std::string param_name;
 };
 
-struct invalid_parameter_value: arbor_exception {
+struct ARB_SYMBOL_VISIBLE invalid_parameter_value: arbor_exception {
     invalid_parameter_value(const std::string& mech_name, const std::string& param_name, const std::string& value_str);
     invalid_parameter_value(const std::string& mech_name, const std::string& param_name, double value);
     std::string mech_name;
@@ -136,32 +137,32 @@ struct invalid_parameter_value: arbor_exception {
     double value;
 };
 
-struct invalid_ion_remap: arbor_exception {
+struct ARB_SYMBOL_VISIBLE invalid_ion_remap: arbor_exception {
     explicit invalid_ion_remap(const std::string& mech_name);
     invalid_ion_remap(const std::string& mech_name, const std::string& from_ion, const std::string& to_ion);
     std::string from_ion;
     std::string to_ion;
 };
 
-struct no_such_implementation: arbor_exception {
+struct ARB_SYMBOL_VISIBLE no_such_implementation: arbor_exception {
     explicit no_such_implementation(const std::string& mech_name);
     std::string mech_name;
 };
 
 // Run-time value bounds check:
 
-struct range_check_failure: arbor_exception {
+struct ARB_SYMBOL_VISIBLE range_check_failure: arbor_exception {
     explicit range_check_failure(const std::string& whatstr, double value);
     double value;
 };
 
-struct file_not_found_error: arbor_exception {
+struct ARB_SYMBOL_VISIBLE file_not_found_error: arbor_exception {
     file_not_found_error(const std::string& fn);
     std::string filename;
 };
 
 //
-struct bad_catalogue_error: arbor_exception {
+struct ARB_SYMBOL_VISIBLE bad_catalogue_error: arbor_exception {
     bad_catalogue_error(const std::string&);
     bad_catalogue_error(const std::string&, const std::any&);
     std::any platform_error;
@@ -169,12 +170,12 @@ struct bad_catalogue_error: arbor_exception {
 
 // ABI errors
 
-struct bad_alignment: arbor_exception {
+struct ARB_SYMBOL_VISIBLE bad_alignment: arbor_exception {
     bad_alignment(size_t);
     size_t alignment;
 };
 
-struct unsupported_abi_error: arbor_exception {
+struct ARB_SYMBOL_VISIBLE unsupported_abi_error: arbor_exception {
     unsupported_abi_error(size_t);
     size_t version;
 };
diff --git a/arbor/include/arbor/assert.hpp b/arbor/include/arbor/assert.hpp
index ac52566964b3c64306ce098bea29d926acebab3d..a400e335ed358e877dc69fde714c08295eb223ac 100644
--- a/arbor/include/arbor/assert.hpp
+++ b/arbor/include/arbor/assert.hpp
@@ -1,5 +1,6 @@
 #pragma once
 
+#include <arbor/export.hpp>
 #include <arbor/assert_macro.hpp>
 
 namespace arb {
@@ -7,10 +8,10 @@ namespace arb {
 using failed_assertion_handler_t =
     void (*)(const char* assertion, const char* file, int line, const char* func);
 
-void abort_on_failed_assertion(const char* assertion, const char* file, int line, const char* func);
-void ignore_failed_assertion(const char* assertion, const char* file, int line, const char* func);
+ARB_ARBOR_API void abort_on_failed_assertion(const char* assertion, const char* file, int line, const char* func);
+ARB_ARBOR_API void ignore_failed_assertion(const char* assertion, const char* file, int line, const char* func);
 
 // defaults to abort_on_failed_assertion;
-extern failed_assertion_handler_t global_failed_assertion_handler;
+ARB_ARBOR_API extern failed_assertion_handler_t global_failed_assertion_handler;
 
 } // namespace arb
diff --git a/arbor/include/arbor/benchmark_cell.hpp b/arbor/include/arbor/benchmark_cell.hpp
index 49d93d9f4f8e5d37d8d0e73284e406e909f67a72..30719fd947653885c18cbd59f583b7f044d10711 100644
--- a/arbor/include/arbor/benchmark_cell.hpp
+++ b/arbor/include/arbor/benchmark_cell.hpp
@@ -1,5 +1,6 @@
 #pragma once
 
+#include <arbor/export.hpp>
 #include <arbor/schedule.hpp>
 
 namespace arb {
@@ -7,7 +8,7 @@ namespace arb {
 // Cell description returned by recipe::cell_description(gid) for cells with
 // recipe::cell_kind(gid) returning cell_kind::benchmark
 
-struct benchmark_cell {
+struct ARB_SYMBOL_VISIBLE benchmark_cell {
     cell_tag_type source; // Label of source.
     cell_tag_type target; // Label of target.
 
diff --git a/arbor/include/arbor/cable_cell.hpp b/arbor/include/arbor/cable_cell.hpp
index 38331263102f1adf8d9f6e82eaaf1ce3ca9f5a61..5b8ef76bacaf143045d3beafe3da3355b5a0393c 100644
--- a/arbor/include/arbor/cable_cell.hpp
+++ b/arbor/include/arbor/cable_cell.hpp
@@ -6,6 +6,7 @@
 #include <variant>
 #include <vector>
 
+#include <arbor/export.hpp>
 #include <arbor/arbexcept.hpp>
 #include <arbor/cable_cell_param.hpp>
 #include <arbor/common_types.hpp>
@@ -49,7 +50,7 @@ using cable_sample_range = std::pair<const double*, const double*>;
 // calls to an attached sampler, one per valid location matched by the expression.
 //
 // Metadata for point process probes.
-struct cable_probe_point_info {
+struct ARB_SYMBOL_VISIBLE cable_probe_point_info {
     cell_lid_type target;   // Target number of point process instance on cell.
     unsigned multiplicity;  // Number of combined instances at this site.
     mlocation loc;          // Point on cell morphology where instance is placed.
@@ -58,48 +59,48 @@ struct cable_probe_point_info {
 // Voltage estimate [mV] at `location`, interpolated.
 // Sample value type: `double`
 // Sample metadata type: `mlocation`
-struct cable_probe_membrane_voltage {
+struct ARB_SYMBOL_VISIBLE cable_probe_membrane_voltage {
     locset locations;
 };
 
 // Voltage estimate [mV], reported against each cable in each control volume. Not interpolated.
 // Sample value type: `cable_sample_range`
 // Sample metadata type: `mcable_list`
-struct cable_probe_membrane_voltage_cell {};
+struct ARB_SYMBOL_VISIBLE cable_probe_membrane_voltage_cell {};
 
 // Axial current estimate [nA] at `location`, interpolated.
 // Sample value type: `double`
 // Sample metadata type: `mlocation`
-struct cable_probe_axial_current {
+struct ARB_SYMBOL_VISIBLE cable_probe_axial_current {
     locset locations;
 };
 
 // Total current density [A/m²] across membrane _excluding_ capacitive and stimulus current at `location`.
 // Sample value type: `cable_sample_range`
 // Sample metadata type: `mlocation`
-struct cable_probe_total_ion_current_density {
+struct ARB_SYMBOL_VISIBLE cable_probe_total_ion_current_density {
     locset locations;
 };
 
 // Total ionic current [nA] across membrane _excluding_ capacitive current across components of the cell.
 // Sample value type: `cable_sample_range`
 // Sample metadata type: `mcable_list`
-struct cable_probe_total_ion_current_cell {};
+struct ARB_SYMBOL_VISIBLE cable_probe_total_ion_current_cell {};
 
 // Total membrane current [nA] across components of the cell _excluding_ stimulus currents.
 // Sample value type: `cable_sample_range`
 // Sample metadata type: `mcable_list`
-struct cable_probe_total_current_cell {};
+struct ARB_SYMBOL_VISIBLE cable_probe_total_current_cell {};
 
 // Stimulus currents [nA] across components of the cell.
 // Sample value type: `cable_sample_range`
 // Sample metadata type: `mcable_list`
-struct cable_probe_stimulus_current_cell {};
+struct ARB_SYMBOL_VISIBLE cable_probe_stimulus_current_cell {};
 
 // Value of state variable `state` in density mechanism `mechanism` in CV at `location`.
 // Sample value type: `double`
 // Sample metadata type: `mlocation`
-struct cable_probe_density_state {
+struct ARB_SYMBOL_VISIBLE cable_probe_density_state {
     locset locations;
     std::string mechanism;
     std::string state;
@@ -108,7 +109,7 @@ struct cable_probe_density_state {
 // Value of state variable `state` in density mechanism `mechanism` across components of the cell.
 // Sample value type: `cable_sample_range`
 // Sample metadata type: `mcable_list`
-struct cable_probe_density_state_cell {
+struct ARB_SYMBOL_VISIBLE cable_probe_density_state_cell {
     std::string mechanism;
     std::string state;
 };
@@ -116,7 +117,7 @@ struct cable_probe_density_state_cell {
 // Value of state variable `key` in point mechanism `source` at target `target`.
 // Sample value type: `double`
 // Sample metadata type: `cable_probe_point_info`
-struct cable_probe_point_state {
+struct ARB_SYMBOL_VISIBLE cable_probe_point_state {
     cell_lid_type target;
     std::string mechanism;
     std::string state;
@@ -126,7 +127,7 @@ struct cable_probe_point_state {
 // Metadata has one entry of type cable_probe_point_info for each matched (possibly coalesced) instance.
 // Sample value type: `cable_sample_range`
 // Sample metadata type: `std::vector<cable_probe_point_info>`
-struct cable_probe_point_state_cell {
+struct ARB_SYMBOL_VISIBLE cable_probe_point_state_cell {
     std::string mechanism;
     std::string state;
 };
@@ -134,7 +135,7 @@ struct cable_probe_point_state_cell {
 // Current density [A/m²] across membrane attributed to the ion `source` at `location`.
 // Sample value type: `double`
 // Sample metadata type: `mlocation`
-struct cable_probe_ion_current_density {
+struct ARB_SYMBOL_VISIBLE cable_probe_ion_current_density {
     locset locations;
     std::string ion;
 };
@@ -142,14 +143,14 @@ struct cable_probe_ion_current_density {
 // Total ionic current [nA] attributed to the ion `source` across components of the cell.
 // Sample value type: `cable_sample_range`
 // Sample metadata type: `mcable_list`
-struct cable_probe_ion_current_cell {
+struct ARB_SYMBOL_VISIBLE cable_probe_ion_current_cell {
     std::string ion;
 };
 
 // Ionic internal concentration [mmol/L] of ion `source` at `location`.
 // Sample value type: `double`
 // Sample metadata type: `mlocation`
-struct cable_probe_ion_int_concentration {
+struct ARB_SYMBOL_VISIBLE cable_probe_ion_int_concentration {
     locset locations;
     std::string ion;
 };
@@ -157,14 +158,14 @@ struct cable_probe_ion_int_concentration {
 // Ionic internal concentration [mmol/L] of ion `source` across components of the cell.
 // Sample value type: `cable_sample_range`
 // Sample metadata type: `mcable_list`
-struct cable_probe_ion_int_concentration_cell {
+struct ARB_SYMBOL_VISIBLE cable_probe_ion_int_concentration_cell {
     std::string ion;
 };
 
 // Ionic external concentration [mmol/L] of ion `source` at `location`.
 // Sample value type: `double`
 // Sample metadata type: `mlocation`
-struct cable_probe_ion_ext_concentration {
+struct ARB_SYMBOL_VISIBLE cable_probe_ion_ext_concentration {
     locset locations;
     std::string ion;
 };
@@ -172,7 +173,7 @@ struct cable_probe_ion_ext_concentration {
 // Ionic external concentration [mmol/L] of ion `source` across components of the cell.
 // Sample value type: `cable_sample_range`
 // Sample metadata type: `mcable_list`
-struct cable_probe_ion_ext_concentration_cell {
+struct ARB_SYMBOL_VISIBLE cable_probe_ion_ext_concentration_cell {
     std::string ion;
 };
 
@@ -220,7 +221,7 @@ using cable_cell_location_map = static_typed_map<location_assignment,
     synapse, junction, i_clamp, threshold_detector>;
 
 // High-level abstract representation of a cell.
-class cable_cell {
+class ARB_SYMBOL_VISIBLE cable_cell {
 public:
     using index_type = cell_lid_type;
     using size_type = cell_local_size_type;
diff --git a/arbor/include/arbor/cable_cell_param.hpp b/arbor/include/arbor/cable_cell_param.hpp
index d1ad2bfa66ec931b53572d2bee91527a39b933c5..7ab612f6b47750a0cf4987c1950d3a6784695fd8 100644
--- a/arbor/include/arbor/cable_cell_param.hpp
+++ b/arbor/include/arbor/cable_cell_param.hpp
@@ -7,6 +7,7 @@
 #include <string>
 #include <variant>
 
+#include <arbor/export.hpp>
 #include <arbor/arbexcept.hpp>
 #include <arbor/cv_policy.hpp>
 #include <arbor/mechcat.hpp>
@@ -16,7 +17,7 @@ namespace arb {
 
 // Specialized arbor exception for errors in cell building.
 
-struct cable_cell_error: arbor_exception {
+struct ARB_SYMBOL_VISIBLE cable_cell_error: arbor_exception {
     cable_cell_error(const std::string& what):
         arbor_exception("cable_cell: "+what) {}
 };
@@ -47,7 +48,7 @@ struct cable_cell_ion_data {
 // Periodic envelopes are not supported, but may well be a feature worth
 // considering in the future.
 
-struct i_clamp {
+struct ARB_SYMBOL_VISIBLE i_clamp {
     struct envelope_point {
         double t;         // [ms]
         double amplitude; // [nA]
@@ -83,40 +84,40 @@ struct i_clamp {
 };
 
 // Threshold detector description.
-struct threshold_detector {
+struct ARB_SYMBOL_VISIBLE threshold_detector {
     double threshold;
 };
 
 // Setter types for painting physical and ion parameters or setting
 // cell-wide default:
 
-struct init_membrane_potential {
+struct ARB_SYMBOL_VISIBLE init_membrane_potential {
     double value = NAN; // [mV]
 };
 
-struct temperature_K {
+struct ARB_SYMBOL_VISIBLE temperature_K {
     double value = NAN; // [K]
 };
 
-struct axial_resistivity {
+struct ARB_SYMBOL_VISIBLE axial_resistivity {
     double value = NAN; // [Ω·cm]
 };
 
-struct membrane_capacitance {
+struct ARB_SYMBOL_VISIBLE membrane_capacitance {
     double value = NAN; // [F/m²]
 };
 
-struct init_int_concentration {
+struct ARB_SYMBOL_VISIBLE init_int_concentration {
     std::string ion = "";
     double value = NAN; // [mM]
 };
 
-struct init_ext_concentration {
+struct ARB_SYMBOL_VISIBLE init_ext_concentration {
     std::string ion = "";
     double value = NAN; // [mM]
 };
 
-struct init_reversal_potential {
+struct ARB_SYMBOL_VISIBLE init_reversal_potential {
     std::string ion = "";
     double value = NAN; // [mV]
 };
@@ -126,7 +127,7 @@ struct init_reversal_potential {
 // density and point mechanisms to segments and
 // reversal potential computations to cells.
 
-struct mechanism_desc {
+struct ARB_SYMBOL_VISIBLE mechanism_desc {
     struct field_proxy {
         mechanism_desc* m;
         std::string key;
@@ -185,7 +186,7 @@ private:
 };
 
 // Tagged mechanism types for dispatching decor::place() and decor::paint() calls
-struct junction {
+struct ARB_SYMBOL_VISIBLE junction {
     mechanism_desc mech;
     explicit junction(mechanism_desc m): mech(std::move(m)) {}
     junction(mechanism_desc m, const std::unordered_map<std::string, double>& params): mech(std::move(m)) {
@@ -195,7 +196,7 @@ struct junction {
     }
 };
 
-struct synapse {
+struct ARB_SYMBOL_VISIBLE synapse {
     mechanism_desc mech;
     explicit synapse(mechanism_desc m): mech(std::move(m)) {}
     synapse(mechanism_desc m, const std::unordered_map<std::string, double>& params): mech(std::move(m)) {
@@ -205,7 +206,7 @@ struct synapse {
     }
 };
 
-struct density {
+struct ARB_SYMBOL_VISIBLE density {
     mechanism_desc mech;
     explicit density(mechanism_desc m): mech(std::move(m)) {}
     density(mechanism_desc m, const std::unordered_map<std::string, double>& params): mech(std::move(m)) {
@@ -215,7 +216,7 @@ struct density {
     }
 };
 
-struct ion_reversal_potential_method {
+struct ARB_SYMBOL_VISIBLE ion_reversal_potential_method {
     std::string ion;
     mechanism_desc method;
 };
@@ -257,7 +258,7 @@ using defaultable =
 // be set locally witihin a cell using the `cable_cell::paint()`, and the
 // cell defaults can be individually set with `cable_cell:set_default()`.
 
-struct cable_cell_parameter_set {
+struct ARB_ARBOR_API cable_cell_parameter_set {
     std::optional<double> init_membrane_potential; // [mV]
     std::optional<double> temperature_K;           // [K]
     std::optional<double> axial_resistivity;       // [Ω·cm]
@@ -273,7 +274,7 @@ struct cable_cell_parameter_set {
 
 // A flat description of defaults, paintings and placings that
 // are to be applied to a morphology in a cable_cell.
-class decor {
+class ARB_ARBOR_API decor {
     std::vector<std::pair<region, paintable>> paintings_;
     std::vector<std::tuple<locset, placeable, cell_tag_type>> placements_;
     cable_cell_parameter_set defaults_;
@@ -288,11 +289,11 @@ public:
     void set_default(defaultable);
 };
 
-extern cable_cell_parameter_set neuron_parameter_defaults;
+ARB_ARBOR_API extern cable_cell_parameter_set neuron_parameter_defaults;
 
 // Global cable cell data.
 
-struct cable_cell_global_properties {
+struct ARB_SYMBOL_VISIBLE cable_cell_global_properties {
     mechanism_catalogue catalogue = global_default_catalogue();
 
     // If >0, check membrane voltage magnitude is less than limit
@@ -329,6 +330,6 @@ struct cable_cell_global_properties {
 
 // Throw cable_cell_error if any default parameters are left unspecified,
 // or if the supplied ion data is incomplete.
-void check_global_properties(const cable_cell_global_properties&);
+ARB_ARBOR_API void check_global_properties(const cable_cell_global_properties&);
 
 } // namespace arb
diff --git a/arbor/include/arbor/common_types.hpp b/arbor/include/arbor/common_types.hpp
index 75141c0626cd3ace24ee2980c16bfb413f4a2702..7db5bcde5af2b840cfd8c8e87d0964bf09a15aaf 100644
--- a/arbor/include/arbor/common_types.hpp
+++ b/arbor/include/arbor/common_types.hpp
@@ -15,6 +15,7 @@
 
 #include <arbor/util/lexcmp_def.hpp>
 #include <arbor/util/hash_def.hpp>
+#include <arbor/export.hpp>
 
 namespace arb {
 
@@ -121,7 +122,7 @@ enum class backend_kind {
 // Enumeration used to indentify the cell type/kind, used by the model to
 // group equal kinds in the same cell group.
 
-enum class cell_kind {
+enum class ARB_SYMBOL_VISIBLE cell_kind {
     cable,   // Our own special mc neuron.
     lif,       // Leaky-integrate and fire neuron.
     spike_source,     // Cell that generates spikes at a user-supplied sequence of time points.
@@ -136,10 +137,10 @@ enum class binning_kind {
     following, // => round times down to previous event if within binning interval.
 };
 
-std::ostream& operator<<(std::ostream& o, lid_selection_policy m);
-std::ostream& operator<<(std::ostream& o, cell_member_type m);
-std::ostream& operator<<(std::ostream& o, cell_kind k);
-std::ostream& operator<<(std::ostream& o, backend_kind k);
+ARB_ARBOR_API std::ostream& operator<<(std::ostream& o, lid_selection_policy m);
+ARB_ARBOR_API std::ostream& operator<<(std::ostream& o, cell_member_type m);
+ARB_ARBOR_API std::ostream& operator<<(std::ostream& o, cell_kind k);
+ARB_ARBOR_API std::ostream& operator<<(std::ostream& o, backend_kind k);
 
 } // namespace arb
 
diff --git a/arbor/include/arbor/communication/mpi_error.hpp b/arbor/include/arbor/communication/mpi_error.hpp
index 911447a1a0357e20e278709704769c5d32101cc7..626c1a5a24c4b3de90cc9b41c121601d1aa14ba4 100644
--- a/arbor/include/arbor/communication/mpi_error.hpp
+++ b/arbor/include/arbor/communication/mpi_error.hpp
@@ -5,6 +5,8 @@
 
 #include <mpi.h>
 
+#include <arbor/export.hpp>
+
 namespace arb {
 
 enum class mpi_errc {
@@ -79,9 +81,9 @@ template <> struct is_error_condition_enum<arb::mpi_errc>: true_type {};
 namespace arb {
 
 class mpi_error_category_impl;
-const mpi_error_category_impl& mpi_error_category();
+ARB_ARBOR_API const mpi_error_category_impl& mpi_error_category();
 
-class mpi_error_category_impl: public std::error_category {
+class ARB_SYMBOL_VISIBLE mpi_error_category_impl: public std::error_category {
     const char* name() const noexcept override;
     std::string message(int) const override;
     std::error_condition default_error_condition(int) const noexcept override;
@@ -91,7 +93,7 @@ inline std::error_condition make_error_condition(mpi_errc ec) {
     return std::error_condition(static_cast<int>(ec), mpi_error_category());
 }
 
-struct mpi_error: std::system_error {
+struct ARB_SYMBOL_VISIBLE mpi_error: std::system_error {
     explicit mpi_error(int mpi_err):
         std::system_error(mpi_err, mpi_error_category()) {}
 
diff --git a/arbor/include/arbor/context.hpp b/arbor/include/arbor/context.hpp
index a61461ef65cf1b7a80a97a3bec281e727f4f00b5..75ba004bc5c5a92a86a1c58aaddbf2d0d2459bdb 100644
--- a/arbor/include/arbor/context.hpp
+++ b/arbor/include/arbor/context.hpp
@@ -2,6 +2,8 @@
 
 #include <memory>
 
+#include <arbor/export.hpp>
+
 namespace arb {
 
 // Requested dry-run parameters.
@@ -49,7 +51,7 @@ struct execution_context;
 //
 // As execution_context is an incomplete type, an explicit deleter must be
 // provided.
-struct execution_context_deleter {
+struct ARB_ARBOR_API execution_context_deleter {
     void operator()(execution_context*) const;
 };
 using context = std::unique_ptr<execution_context, execution_context_deleter>;
@@ -57,20 +59,20 @@ using context = std::unique_ptr<execution_context, execution_context_deleter>;
 // Helpers for creating contexts. These are implemented in the back end.
 
 // Non-distributed context using the requested resources.
-context make_context(const proc_allocation& resources = proc_allocation{});
+ARB_ARBOR_API context make_context(const proc_allocation& resources = proc_allocation{});
 
 // Distributed context that uses MPI communicator comm, and local resources
 // described by resources. Or dry run context that uses dry_run_info.
 template <typename Comm>
-context make_context(const proc_allocation& resources, Comm comm);
+ARB_ARBOR_API context make_context(const proc_allocation& resources, Comm comm);
 
 // Queries for properties of execution resources in a context.
 
-std::string distribution_type(const context&);
-bool has_gpu(const context&);
-unsigned num_threads(const context&);
-bool has_mpi(const context&);
-unsigned num_ranks(const context&);
-unsigned rank(const context&);
+ARB_ARBOR_API std::string distribution_type(const context&);
+ARB_ARBOR_API bool has_gpu(const context&);
+ARB_ARBOR_API unsigned num_threads(const context&);
+ARB_ARBOR_API bool has_mpi(const context&);
+ARB_ARBOR_API unsigned num_ranks(const context&);
+ARB_ARBOR_API unsigned rank(const context&);
 
 }
diff --git a/arbor/include/arbor/cv_policy.hpp b/arbor/include/arbor/cv_policy.hpp
index 917610a1d18ff5ff1ced7f04ab28604453747080..a1a5077cf17dde69e9c21e5489bff15c1f17bc05 100644
--- a/arbor/include/arbor/cv_policy.hpp
+++ b/arbor/include/arbor/cv_policy.hpp
@@ -3,6 +3,7 @@
 #include <memory>
 #include <utility>
 
+#include <arbor/export.hpp>
 #include <arbor/morph/region.hpp>
 #include <arbor/morph/locset.hpp>
 
@@ -70,7 +71,7 @@ struct cv_policy_base {
 
 using cv_policy_base_ptr = std::unique_ptr<cv_policy_base>;
 
-struct cv_policy {
+struct ARB_SYMBOL_VISIBLE cv_policy {
     cv_policy(const cv_policy_base& ref) { // implicit
         policy_ptr = ref.clone();
     }
@@ -102,8 +103,8 @@ private:
     cv_policy_base_ptr policy_ptr;
 };
 
-cv_policy operator+(const cv_policy&, const cv_policy&);
-cv_policy operator|(const cv_policy&, const cv_policy&);
+ARB_ARBOR_API cv_policy operator+(const cv_policy&, const cv_policy&);
+ARB_ARBOR_API cv_policy operator|(const cv_policy&, const cv_policy&);
 
 
 // Common flags for CV policies; bitwise composable.
@@ -115,7 +116,7 @@ namespace cv_policy_flag {
     };
 }
 
-struct cv_policy_explicit: cv_policy_base {
+struct ARB_ARBOR_API cv_policy_explicit: cv_policy_base {
     explicit cv_policy_explicit(locset locs, region domain = reg::all()):
         locs_(std::move(locs)), domain_(std::move(domain)) {}
 
@@ -132,7 +133,7 @@ private:
     region domain_;
 };
 
-struct cv_policy_single: cv_policy_base {
+struct ARB_ARBOR_API cv_policy_single: cv_policy_base {
     explicit cv_policy_single(region domain = reg::all()):
         domain_(domain) {}
 
@@ -148,7 +149,7 @@ private:
     region domain_;
 };
 
-struct cv_policy_max_extent: cv_policy_base {
+struct ARB_ARBOR_API cv_policy_max_extent: cv_policy_base {
     cv_policy_max_extent(double max_extent, region domain, cv_policy_flag::value flags = cv_policy_flag::none):
          max_extent_(max_extent), domain_(std::move(domain)), flags_(flags) {}
 
@@ -169,7 +170,7 @@ private:
     cv_policy_flag::value flags_;
 };
 
-struct cv_policy_fixed_per_branch: cv_policy_base {
+struct ARB_ARBOR_API cv_policy_fixed_per_branch: cv_policy_base {
     cv_policy_fixed_per_branch(unsigned cv_per_branch, region domain, cv_policy_flag::value flags = cv_policy_flag::none):
          cv_per_branch_(cv_per_branch), domain_(std::move(domain)), flags_(flags) {}
 
@@ -190,7 +191,7 @@ private:
     cv_policy_flag::value flags_;
 };
 
-struct cv_policy_every_segment: cv_policy_base {
+struct ARB_ARBOR_API cv_policy_every_segment: cv_policy_base {
     explicit cv_policy_every_segment(region domain = reg::all()):
          domain_(std::move(domain)) {}
 
diff --git a/arbor/include/arbor/domain_decomposition.hpp b/arbor/include/arbor/domain_decomposition.hpp
index c79800503542064977af8ca539f0b0d64a891b7c..0de6383ae7bf77341008386413eb890c4715a6d3 100644
--- a/arbor/include/arbor/domain_decomposition.hpp
+++ b/arbor/include/arbor/domain_decomposition.hpp
@@ -7,6 +7,7 @@
 #include <arbor/assert.hpp>
 #include <arbor/common_types.hpp>
 #include <arbor/context.hpp>
+#include <arbor/export.hpp>
 #include <arbor/recipe.hpp>
 
 namespace arb {
@@ -32,7 +33,7 @@ struct group_description {
 /// distribution of cells across cell_groups and domains.
 /// A load balancing algorithm is responsible for generating the
 /// domain_decomposition, e.g. arb::partitioned_load_balancer().
-class domain_decomposition {
+class ARB_ARBOR_API domain_decomposition {
 public:
     domain_decomposition() = delete;
     domain_decomposition(const recipe& rec, const context& ctx, const std::vector<group_description>& groups);
diff --git a/arbor/include/arbor/domdecexcept.hpp b/arbor/include/arbor/domdecexcept.hpp
index 206f1c7628c2dba7eee780ca5cc37c5627cd9085..aac8490733eaf257b6ee37e298f112fb2cdbe276 100644
--- a/arbor/include/arbor/domdecexcept.hpp
+++ b/arbor/include/arbor/domdecexcept.hpp
@@ -2,41 +2,42 @@
 
 #include <string>
 
+#include <arbor/export.hpp>
 #include <arbor/arbexcept.hpp>
 #include <arbor/domain_decomposition.hpp>
 
 namespace arb {
-struct dom_dec_exception: public arbor_exception {
+struct ARB_SYMBOL_VISIBLE dom_dec_exception: public arbor_exception {
     dom_dec_exception(const std::string& what): arbor_exception("Invalid domain decomposition: " + what) {}
 };
 
-struct invalid_gj_cell_group: dom_dec_exception {
+struct ARB_SYMBOL_VISIBLE invalid_gj_cell_group: dom_dec_exception {
     invalid_gj_cell_group(cell_gid_type gid_0, cell_gid_type gid_1);
     cell_gid_type gid_0, gid_1;
 };
 
-struct invalid_sum_local_cells: dom_dec_exception {
+struct ARB_SYMBOL_VISIBLE invalid_sum_local_cells: dom_dec_exception {
     invalid_sum_local_cells(unsigned gc_wrong, unsigned gc_right);
     unsigned gc_wrong, gc_right;
 };
 
-struct duplicate_gid: dom_dec_exception {
+struct ARB_SYMBOL_VISIBLE duplicate_gid: dom_dec_exception {
     duplicate_gid(cell_gid_type gid);
     cell_gid_type gid;
 };
 
-struct out_of_bounds: dom_dec_exception {
+struct ARB_SYMBOL_VISIBLE out_of_bounds: dom_dec_exception {
     out_of_bounds(cell_gid_type gid, unsigned num_cells);
     cell_gid_type gid;
     unsigned num_cells;
 };
 
-struct invalid_backend: dom_dec_exception {
+struct ARB_SYMBOL_VISIBLE invalid_backend: dom_dec_exception {
     invalid_backend(int rank);
     int rank;
 };
 
-struct incompatible_backend: dom_dec_exception {
+struct ARB_SYMBOL_VISIBLE incompatible_backend: dom_dec_exception {
     incompatible_backend(int rank, cell_kind kind);
     int rank;
     cell_kind kind;
diff --git a/arbor/include/arbor/gpu/cuda_api.hpp b/arbor/include/arbor/gpu/cuda_api.hpp
index 12bfa476afe30d7ed67dcfc8fb4e9330d99ef7e5..eaf4c5005dea796d5d5f963117f0556586db6e1a 100644
--- a/arbor/include/arbor/gpu/cuda_api.hpp
+++ b/arbor/include/arbor/gpu/cuda_api.hpp
@@ -5,6 +5,8 @@
 #include <cuda_runtime.h>
 #include <cuda_runtime_api.h>
 
+#include <arbor/export.hpp>
+
 namespace arb {
 namespace gpu {
 
@@ -12,7 +14,7 @@ namespace gpu {
 
 using DeviceProp = cudaDeviceProp;
 
-struct api_error_type {
+struct ARB_SYMBOL_VISIBLE api_error_type {
     cudaError_t value;
     api_error_type(cudaError_t e): value(e) {}
 
diff --git a/arbor/include/arbor/gpu/hip_api.hpp b/arbor/include/arbor/gpu/hip_api.hpp
index 283f68bf4a90ff16e87f99dbbbd3dc92a0850161..55c27c3e95221c07838af2bb1e79083ada83d279 100644
--- a/arbor/include/arbor/gpu/hip_api.hpp
+++ b/arbor/include/arbor/gpu/hip_api.hpp
@@ -4,6 +4,8 @@
 #include <hip/hip_runtime.h>
 #include <hip/hip_runtime_api.h>
 
+#include <arbor/export.hpp>
+
 namespace arb {
 namespace gpu {
 
@@ -11,7 +13,7 @@ namespace gpu {
 
 using DeviceProp = hipDeviceProp_t;
 
-struct api_error_type {
+struct ARB_SYMBOL_VISIBLE api_error_type {
     hipError_t value;
     api_error_type(hipError_t e): value(e) {}
 
diff --git a/arbor/include/arbor/lif_cell.hpp b/arbor/include/arbor/lif_cell.hpp
index 35c2fa98809bf6c0c2c0c4987132fc09b2c129af..cff3eeb55104b4b6da453d661e6a8b467a61c297 100644
--- a/arbor/include/arbor/lif_cell.hpp
+++ b/arbor/include/arbor/lif_cell.hpp
@@ -1,11 +1,12 @@
 #pragma once
 
 #include <arbor/common_types.hpp>
+#include <arbor/export.hpp>
 
 namespace arb {
 
 // Model parameters of leaky integrate and fire neuron model.
-struct lif_cell {
+struct ARB_SYMBOL_VISIBLE lif_cell {
     cell_tag_type source; // Label of source.
     cell_tag_type target; // Label of target.
 
diff --git a/arbor/include/arbor/load_balance.hpp b/arbor/include/arbor/load_balance.hpp
index 44d966e4cf7d81f17d7a304f97f5136d6aef427c..b9e8e71b25a9bd75e6aa7487e897c5cc099f3191 100644
--- a/arbor/include/arbor/load_balance.hpp
+++ b/arbor/include/arbor/load_balance.hpp
@@ -3,6 +3,7 @@
 #include <cstddef>
 #include <unordered_map>
 
+#include <arbor/export.hpp>
 #include <arbor/context.hpp>
 #include <arbor/domain_decomposition.hpp>
 #include <arbor/recipe.hpp>
@@ -19,7 +20,7 @@ struct partition_hint {
 
 using partition_hint_map = std::unordered_map<cell_kind, partition_hint>;
 
-domain_decomposition partition_load_balance(
+ARB_ARBOR_API domain_decomposition partition_load_balance(
     const recipe& rec,
     const context& ctx,
     partition_hint_map hint_map = {});
diff --git a/arbor/include/arbor/mechcat.hpp b/arbor/include/arbor/mechcat.hpp
index 224437049ffaffce172d7005ad43694f78747231..c7811725bea6b7051292ef13a569048e6edbbf16 100644
--- a/arbor/include/arbor/mechcat.hpp
+++ b/arbor/include/arbor/mechcat.hpp
@@ -6,6 +6,7 @@
 #include <typeindex>
 #include <vector>
 
+#include <arbor/export.hpp>
 #include <arbor/mechinfo.hpp>
 #include <arbor/mechanism.hpp>
 #include <arbor/mechanism_abi.h>
@@ -39,7 +40,7 @@ namespace arb {
 // catalogue_state comprises the private implementation of mechanism_catalogue.
 struct catalogue_state;
 
-class mechanism_catalogue {
+class ARB_ARBOR_API mechanism_catalogue {
 public:
     using value_type = double;
 
@@ -109,11 +110,11 @@ private:
 };
 
 // References to global mechanism catalogues.
-const mechanism_catalogue& global_default_catalogue();
-const mechanism_catalogue& global_allen_catalogue();
-const mechanism_catalogue& global_bbp_catalogue();
+ARB_ARBOR_API const mechanism_catalogue& global_default_catalogue();
+ARB_ARBOR_API const mechanism_catalogue& global_allen_catalogue();
+ARB_ARBOR_API const mechanism_catalogue& global_bbp_catalogue();
 
 // Load catalogue from disk.
-const mechanism_catalogue& load_catalogue(const std::string&);
+ARB_ARBOR_API const mechanism_catalogue& load_catalogue(const std::string&);
 
 } // namespace arb
diff --git a/arbor/include/arbor/mechinfo.hpp b/arbor/include/arbor/mechinfo.hpp
index f0b552957a08e802ee99c90bc2aaa4beae63444c..39678e4c4964a49243c77c30ee708098a6da1be1 100644
--- a/arbor/include/arbor/mechinfo.hpp
+++ b/arbor/include/arbor/mechinfo.hpp
@@ -10,6 +10,7 @@
 #include <utility>
 #include <vector>
 
+#include <arbor/export.hpp>
 #include <arbor/mechanism_abi.h>
 
 namespace arb {
@@ -52,7 +53,7 @@ struct ion_dependency {
 // Use a textual representation to ease readability.
 using mechanism_fingerprint = std::string;
 
-struct mechanism_info {
+struct ARB_ARBOR_API mechanism_info {
     // mechanism_info is a convenient subset of the ABI mech description
     mechanism_info(const arb_mechanism_type&);
     mechanism_info() = default;
diff --git a/arbor/include/arbor/morph/cv_data.hpp b/arbor/include/arbor/morph/cv_data.hpp
index 58dc1e7c139d04abe9ef3856a79e15bc5a12ef9a..38f24e72c19ef1cdf670fa3a8c8c7882ceb188a1 100644
--- a/arbor/include/arbor/morph/cv_data.hpp
+++ b/arbor/include/arbor/morph/cv_data.hpp
@@ -2,6 +2,7 @@
 
 #include <vector>
 
+#include <arbor/export.hpp>
 #include <arbor/cable_cell.hpp>
 #include <arbor/fvm_types.hpp>
 #include <arbor/morph/embed_pwlin.hpp>
@@ -14,7 +15,7 @@ namespace arb {
 struct cell_cv_data_impl;
 
 // Stores info about the CV geometry of a discretized cable-cell
-class cell_cv_data {
+class ARB_ARBOR_API cell_cv_data {
 public:
     // Returns mcables comprising the CV at a given index.
     mcable_list cables(fvm_size_type index) const;
@@ -47,8 +48,8 @@ struct cv_proportion {
 };
 
 // Construct cell_cv_geometry for cell from default cell discretization if it exists.
-std::optional<cell_cv_data> cv_data(const cable_cell& cell);
+ARB_ARBOR_API std::optional<cell_cv_data> cv_data(const cable_cell& cell);
 
-std::vector<cv_proportion> intersect_region(const region& reg, const cell_cv_data& cvs, bool intergrate_by_length = false);
+ARB_ARBOR_API std::vector<cv_proportion> intersect_region(const region& reg, const cell_cv_data& cvs, bool intergrate_by_length = false);
 
 } //namespace arb
diff --git a/arbor/include/arbor/morph/embed_pwlin.hpp b/arbor/include/arbor/morph/embed_pwlin.hpp
index 412e8997e91ea7c29515b85560ca3590a03da8ad..0fe4bf8dc43dcbe9c8b335c871b245fe84e7fa55 100644
--- a/arbor/include/arbor/morph/embed_pwlin.hpp
+++ b/arbor/include/arbor/morph/embed_pwlin.hpp
@@ -4,6 +4,7 @@
 
 #include <vector>
 
+#include <arbor/export.hpp>
 #include <arbor/morph/morphology.hpp>
 #include <arbor/morph/primitives.hpp>
 
@@ -19,7 +20,7 @@ template <typename X> struct pw_elements;
 // values defined over contiguous intervals.
 using pw_constant_fn = util::pw_elements<double>;
 
-struct embed_pwlin {
+struct ARB_ARBOR_API embed_pwlin {
     explicit embed_pwlin(const arb::morphology& m);
 
     // Segment queries.
diff --git a/arbor/include/arbor/morph/label_dict.hpp b/arbor/include/arbor/morph/label_dict.hpp
index a68bab2b46d70e6fa68444ba74b873816b3a1924..6b590b7c44d3594c42cb3985251db245d6a35d54 100644
--- a/arbor/include/arbor/morph/label_dict.hpp
+++ b/arbor/include/arbor/morph/label_dict.hpp
@@ -4,12 +4,13 @@
 #include <optional>
 #include <unordered_map>
 
+#include <arbor/export.hpp>
 #include <arbor/morph/locset.hpp>
 #include <arbor/morph/region.hpp>
 
 namespace arb {
 
-class label_dict {
+class ARB_ARBOR_API label_dict {
     using ps_map = std::unordered_map<std::string, arb::locset>;
     using reg_map = std::unordered_map<std::string, arb::region>;
     ps_map locsets_;
diff --git a/arbor/include/arbor/morph/locset.hpp b/arbor/include/arbor/morph/locset.hpp
index e449df768a35107c14caa9487eeb3128835751ad..ec127aea168532b7262d620d40b429f4b02d737c 100644
--- a/arbor/include/arbor/morph/locset.hpp
+++ b/arbor/include/arbor/morph/locset.hpp
@@ -8,6 +8,7 @@
 #include <utility>
 #include <vector>
 
+#include <arbor/export.hpp>
 #include <arbor/morph/primitives.hpp>
 #include <arbor/morph/morphology.hpp>
 
@@ -18,7 +19,7 @@ struct mprovider;
 class locset;
 class locset_tag {};
 
-class locset {
+class ARB_SYMBOL_VISIBLE locset {
 public:
     template <typename Impl,
               typename = std::enable_if_t<std::is_base_of<locset_tag, std::decay_t<Impl>>::value>>
@@ -116,66 +117,66 @@ class region;
 namespace ls {
 
 // Explicit location on morphology.
-locset location(msize_t branch, double pos);
+ARB_ARBOR_API locset location(msize_t branch, double pos);
 
 // Set of terminal nodes on a morphology.
-locset terminal();
+ARB_ARBOR_API locset terminal();
 
 // The root node of a morphology.
-locset root();
+ARB_ARBOR_API locset root();
 
 // Named locset.
-locset named(std::string);
+ARB_ARBOR_API locset named(std::string);
 
 // The null (empty) set.
-locset nil();
+ARB_ARBOR_API locset nil();
 
 // Most distal points of a region.
-locset most_distal(region reg);
+ARB_ARBOR_API locset most_distal(region reg);
 
 // Most proximal points of a region.
-locset most_proximal(region reg);
+ARB_ARBOR_API locset most_proximal(region reg);
 
 // Translate locations in locset distance μm in the distal direction
-locset distal_translate(locset ls, double distance);
+ARB_ARBOR_API locset distal_translate(locset ls, double distance);
 
 // Translate locations in locset distance μm in the proximal direction
-locset proximal_translate(locset ls, double distance);
+ARB_ARBOR_API locset proximal_translate(locset ls, double distance);
 
 // Boundary points of a region.
-locset boundary(region reg);
+ARB_ARBOR_API locset boundary(region reg);
 
 // Completed boundary points of a region.
 // (Boundary of completed components.)
-locset cboundary(region reg);
+ARB_ARBOR_API locset cboundary(region reg);
 
 // Returns all locations in a locset that are also in the region.
-locset restrict(locset ls, region reg);
+ARB_ARBOR_API locset restrict(locset ls, region reg);
 
 // Returns locations that mark the segments.
-locset segment_boundaries();
+ARB_ARBOR_API locset segment_boundaries();
 
 // A range `left` to `right` of randomly selected locations with a
 // uniform distribution from region `reg` generated using `seed`
-locset uniform(region reg, unsigned left, unsigned right, uint64_t seed);
+ARB_ARBOR_API locset uniform(region reg, unsigned left, unsigned right, uint64_t seed);
 
 // Proportional location on every branch.
-locset on_branches(double pos);
+ARB_ARBOR_API locset on_branches(double pos);
 
 // Proportional locations on each component:
 // For each component C of the region, find locations L
 // s.t. dist(h, L) = r * max {dist(h, t) | t is a distal point in C}.
-locset on_components(double relpos, region reg);
+ARB_ARBOR_API locset on_components(double relpos, region reg);
 
 // Set of locations in the locset with duplicates removed, i.e. the support of the input multiset
-locset support(locset);
+ARB_ARBOR_API locset support(locset);
 
 } // namespace ls
 
 // Union of two locsets.
-locset join(locset, locset);
+ARB_ARBOR_API locset join(locset, locset);
 
 // Multiset sum of two locsets.
-locset sum(locset, locset);
+ARB_ARBOR_API locset sum(locset, locset);
 
 } // namespace arb
diff --git a/arbor/include/arbor/morph/morphexcept.hpp b/arbor/include/arbor/morph/morphexcept.hpp
index 13963c207c7fd8f07b0814a2f4a148f2b0a163ba..8d1f8f5ce8f68eb0115e9f98217a951194721ae6 100644
--- a/arbor/include/arbor/morph/morphexcept.hpp
+++ b/arbor/include/arbor/morph/morphexcept.hpp
@@ -2,82 +2,83 @@
 
 #include <string>
 
+#include <arbor/export.hpp>
 #include <arbor/arbexcept.hpp>
 #include <arbor/morph/primitives.hpp>
 
 namespace arb {
 
-struct morphology_error: public arbor_exception {
+struct ARB_SYMBOL_VISIBLE morphology_error: public arbor_exception {
     morphology_error(const std::string& what): arbor_exception(what) {}
 };
 
-struct invalid_mlocation: morphology_error {
+struct ARB_SYMBOL_VISIBLE invalid_mlocation: morphology_error {
     invalid_mlocation(mlocation loc);
     mlocation loc;
 };
 
-struct no_such_branch: morphology_error {
+struct ARB_SYMBOL_VISIBLE no_such_branch: morphology_error {
     no_such_branch(msize_t bid);
     msize_t bid;
 };
 
-struct no_such_segment: arbor_exception {
+struct ARB_SYMBOL_VISIBLE no_such_segment: arbor_exception {
     explicit no_such_segment(msize_t sid);
     msize_t sid;
 };
 
-struct invalid_mcable: morphology_error {
+struct ARB_SYMBOL_VISIBLE invalid_mcable: morphology_error {
     invalid_mcable(mcable cable);
     mcable cable;
 };
 
-struct invalid_mcable_list: morphology_error {
+struct ARB_SYMBOL_VISIBLE invalid_mcable_list: morphology_error {
     invalid_mcable_list();
 };
 
-struct invalid_segment_parent: morphology_error {
+struct ARB_SYMBOL_VISIBLE invalid_segment_parent: morphology_error {
     invalid_segment_parent(msize_t parent, msize_t tree_size);
     msize_t parent;
     msize_t tree_size;
 };
 
-struct duplicate_stitch_id: morphology_error {
+struct ARB_SYMBOL_VISIBLE duplicate_stitch_id: morphology_error {
     duplicate_stitch_id(const std::string& id);
     std::string id;
 };
 
-struct no_such_stitch: morphology_error {
+struct ARB_SYMBOL_VISIBLE no_such_stitch: morphology_error {
     no_such_stitch(const std::string& id);
     std::string id;
 };
 
-struct missing_stitch_start: morphology_error {
+struct ARB_SYMBOL_VISIBLE missing_stitch_start: morphology_error {
     missing_stitch_start(const std::string& id);
     std::string id;
 };
 
-struct invalid_stitch_position: morphology_error {
+struct ARB_SYMBOL_VISIBLE invalid_stitch_position: morphology_error {
     invalid_stitch_position(const std::string& id, double along);
     std::string id;
     double along;
 };
 
-struct label_type_mismatch: morphology_error {
+struct ARB_SYMBOL_VISIBLE label_type_mismatch: morphology_error {
     label_type_mismatch(const std::string& label);
     std::string label;
 };
 
-struct incomplete_branch: morphology_error {
+struct ARB_SYMBOL_VISIBLE incomplete_branch: morphology_error {
     incomplete_branch(msize_t bid);
     msize_t bid;
 };
 
-struct unbound_name: morphology_error {
+struct ARB_SYMBOL_VISIBLE unbound_name: morphology_error {
     unbound_name(const std::string& name);
     std::string name;
 };
 
-struct circular_definition: morphology_error {
+struct ARB_SYMBOL_VISIBLE circular_definition: morphology_error {
     circular_definition(const std::string& name);
     std::string name;
 };
diff --git a/arbor/include/arbor/morph/morphology.hpp b/arbor/include/arbor/morph/morphology.hpp
index 18da8ee386fdf2fadc6cd495f065d26a90eea8e6..b12f9b982732ed82079c283ccb794870372bf00d 100644
--- a/arbor/include/arbor/morph/morphology.hpp
+++ b/arbor/include/arbor/morph/morphology.hpp
@@ -4,6 +4,7 @@
 #include <ostream>
 #include <vector>
 
+#include <arbor/export.hpp>
 #include <arbor/morph/primitives.hpp>
 #include <arbor/morph/segment_tree.hpp>
 #include <arbor/util/lexcmp_def.hpp>
@@ -12,7 +13,7 @@ namespace arb {
 
 struct morphology_impl;
 
-class morphology {
+class ARB_ARBOR_API morphology {
     // Hold an immutable copy of the morphology implementation.
     std::shared_ptr<const morphology_impl> impl_;
 
@@ -54,7 +55,7 @@ public:
 // without a morphology.
 // A morphology is required to assert the invariant that an mextent does
 // not contain branches not in the morphology.
-struct mextent {
+struct ARB_ARBOR_API mextent {
     mextent() = default;
     mextent(const mextent&) = default;
     mextent(mextent&&) = default;
@@ -107,19 +108,15 @@ private:
 
 // Morphology utility functions.
 
-mlocation canonical(const morphology&, mlocation);
+ARB_ARBOR_API mlocation canonical(const morphology&, mlocation);
 
 // Find the set of locations in an mlocation_list for which there
 // are no other locations that are more proximal in that list.
-mlocation_list minset(const morphology&, const mlocation_list&);
+ARB_ARBOR_API mlocation_list minset(const morphology&, const mlocation_list&);
 
 // Find the set of locations in an mlocation_list for which there
 // are no other locations that are more distal in the list.
-mlocation_list maxset(const morphology&, const mlocation_list&);
-
-// Reduced representation of an extent, excluding zero-length cables
-// that are covered by more proximal or non-zero-length cables.
-mcable_list canonical(const morphology& m, const mextent& a);
+ARB_ARBOR_API mlocation_list maxset(const morphology&, const mlocation_list&);
 
 // Determine the components of an extent.
 //
@@ -148,7 +145,7 @@ mcable_list canonical(const morphology& m, const mextent& a);
 // directed-path-connected in X, and such that for all x in E_i and all y in
 // E_j, with i not equal to j, x and y are not directed-path-connected in X.
 
-std::vector<mextent> components(const morphology& m, const mextent&);
+ARB_ARBOR_API std::vector<mextent> components(const morphology& m, const mextent&);
 
 
 } // namespace arb
diff --git a/arbor/include/arbor/morph/mprovider.hpp b/arbor/include/arbor/morph/mprovider.hpp
index 7cc3e1262c17de511bf5b326d3534f29c9616bdc..441ca2b580ad2bff2d912928162164541ac0506e 100644
--- a/arbor/include/arbor/morph/mprovider.hpp
+++ b/arbor/include/arbor/morph/mprovider.hpp
@@ -3,6 +3,7 @@
 #include <string>
 #include <unordered_map>
 
+#include <arbor/export.hpp>
 #include <arbor/morph/embed_pwlin.hpp>
 #include <arbor/morph/primitives.hpp>
 #include <arbor/morph/label_dict.hpp>
@@ -12,7 +13,7 @@ namespace arb {
 
 using concrete_embedding = embed_pwlin;
 
-struct mprovider {
+struct ARB_ARBOR_API mprovider {
     mprovider(arb::morphology m, const label_dict& dict): mprovider(m, &dict) {}
     explicit mprovider(arb::morphology m): mprovider(m, nullptr) {}
 
diff --git a/arbor/include/arbor/morph/place_pwlin.hpp b/arbor/include/arbor/morph/place_pwlin.hpp
index 223c287acbdf0b9f60bfc17bc0655aa0b6ed06e4..73c0d5e0b5a532ee488d1e4fabc4de5c9c9d6ace 100644
--- a/arbor/include/arbor/morph/place_pwlin.hpp
+++ b/arbor/include/arbor/morph/place_pwlin.hpp
@@ -7,6 +7,7 @@
 #include <limits>
 #include <utility>
 
+#include <arbor/export.hpp>
 #include <arbor/morph/morphology.hpp>
 #include <arbor/morph/primitives.hpp>
 #include <arbor/math.hpp>
@@ -70,7 +71,7 @@ public:
 
 struct place_pwlin_data;
 
-struct place_pwlin {
+struct ARB_ARBOR_API place_pwlin {
     explicit place_pwlin(const morphology& m, const isometry& iso = isometry{});
 
     // Any point corresponding to the location loc.
diff --git a/arbor/include/arbor/morph/primitives.hpp b/arbor/include/arbor/morph/primitives.hpp
index 919730a1e9c79642dddfa0e44773496f5373a6a6..2cfff2ef4f507f221f237fcc48a59edbe9be0f5d 100644
--- a/arbor/include/arbor/morph/primitives.hpp
+++ b/arbor/include/arbor/morph/primitives.hpp
@@ -5,6 +5,7 @@
 #include <ostream>
 #include <vector>
 
+#include <arbor/export.hpp>
 #include <arbor/util/hash_def.hpp>
 #include <arbor/util/lexcmp_def.hpp>
 
@@ -18,7 +19,7 @@ using msize_t = std::uint32_t;
 constexpr msize_t mnpos = msize_t(-1);
 
 // a morphology sample point: a 3D location and radius.
-struct mpoint {
+struct ARB_SYMBOL_VISIBLE mpoint {
     double x, y, z;  // [µm]
     double radius;   // [μm]
 
@@ -32,9 +33,9 @@ struct mpoint {
     }
 };
 
-mpoint lerp(const mpoint& a, const mpoint& b, double u);
-bool is_collocated(const mpoint& a, const mpoint& b);
-double distance(const mpoint& a, const mpoint& b);
+ARB_ARBOR_API mpoint lerp(const mpoint& a, const mpoint& b, double u);
+ARB_ARBOR_API bool is_collocated(const mpoint& a, const mpoint& b);
+ARB_ARBOR_API double distance(const mpoint& a, const mpoint& b);
 
 // Indicate allowed comparison operations for classifying regions
 enum class comp_op {
@@ -45,7 +46,7 @@ enum class comp_op {
 };
 
 // Describe a cable segment between two adjacent samples.
-struct msegment {
+struct ARB_SYMBOL_VISIBLE msegment {
     msize_t id;
     mpoint prox;
     mpoint dist;
@@ -56,7 +57,7 @@ struct msegment {
 
 
 // Describe a specific location on a morpholology.
-struct mlocation {
+struct ARB_SYMBOL_VISIBLE mlocation {
     // The id of the branch.
     msize_t branch;
     // The relative position on the branch ∈ [0,1].
@@ -66,21 +67,21 @@ struct mlocation {
 };
 
 // branch ≠ npos and 0 ≤ pos ≤ 1
-bool test_invariants(const mlocation&);
+ARB_ARBOR_API bool test_invariants(const mlocation&);
 ARB_DEFINE_LEXICOGRAPHIC_ORDERING(mlocation, (a.branch,a.pos), (b.branch,b.pos));
 
 using mlocation_list = std::vector<mlocation>;
-std::ostream& operator<<(std::ostream& o, const mlocation_list& l);
+ARB_ARBOR_API std::ostream& operator<<(std::ostream& o, const mlocation_list& l);
 
-// Tests whether each location in the list satisfies the invariants for a location,
-// and that the locations in the vector are ordered.
-bool test_invariants(const mlocation_list&);
+//// Tests whether each location in the list satisfies the invariants for a location,
+//// and that the locations in the vector are ordered.
+//bool test_invariants(const mlocation_list&);
 
 // Multiset operations on location lists.
-mlocation_list sum(const mlocation_list&, const mlocation_list&);
-mlocation_list join(const mlocation_list&, const mlocation_list&);
-mlocation_list intersection(const mlocation_list&, const mlocation_list&);
-mlocation_list support(mlocation_list);
+ARB_ARBOR_API mlocation_list sum(const mlocation_list&, const mlocation_list&);
+ARB_ARBOR_API mlocation_list join(const mlocation_list&, const mlocation_list&);
+ARB_ARBOR_API mlocation_list intersection(const mlocation_list&, const mlocation_list&);
+ARB_ARBOR_API mlocation_list support(mlocation_list);
 
 // Describe an unbranched cable in the morphology.
 //
@@ -88,7 +89,7 @@ mlocation_list support(mlocation_list);
 // They may be zero-length, and fork points in the morphology may have multiple,
 // equivalent zero-length cable representations.
 
-struct mcable {
+struct ARB_SYMBOL_VISIBLE mcable {
     // The id of the branch on which the cable lies.
     msize_t branch;
 
@@ -112,10 +113,10 @@ struct mcable {
 ARB_DEFINE_LEXICOGRAPHIC_ORDERING(mcable, (a.branch,a.prox_pos,a.dist_pos), (b.branch,b.prox_pos,b.dist_pos));
 
 using mcable_list = std::vector<mcable>;
-std::ostream& operator<<(std::ostream& o, const mcable_list& c);
+ARB_ARBOR_API std::ostream& operator<<(std::ostream& o, const mcable_list& c);
 // Tests whether each cable in the list satisfies the invariants for a cable,
 // and that the cables in the vector are ordered.
-bool test_invariants(const mcable_list&);
+ARB_ARBOR_API bool test_invariants(const mcable_list&);
 
 } // namespace arb
 
diff --git a/arbor/include/arbor/morph/region.hpp b/arbor/include/arbor/morph/region.hpp
index 0f048860b3ca2b954bf68c0d63504e3b2a80fcb2..0439e23db09a47c873c8517b932b1dcda847f101 100644
--- a/arbor/include/arbor/morph/region.hpp
+++ b/arbor/include/arbor/morph/region.hpp
@@ -8,6 +8,7 @@
 #include <utility>
 #include <vector>
 
+#include <arbor/export.hpp>
 #include <arbor/morph/primitives.hpp>
 #include <arbor/morph/morphology.hpp>
 
@@ -16,7 +17,7 @@ namespace arb {
 struct mprovider;
 struct region_tag {};
 
-class region {
+class ARB_SYMBOL_VISIBLE region {
 public:
     template <typename Impl,
               typename = std::enable_if_t<std::is_base_of<region_tag, std::decay_t<Impl>>::value>>
@@ -120,64 +121,64 @@ class locset;
 namespace reg {
 
 // An empty region.
-region nil();
+ARB_ARBOR_API region nil();
 
 // An explicit cable section.
-region cable(msize_t, double, double);
+ARB_ARBOR_API region cable(msize_t, double, double);
 
 // An explicit branch.
-region branch(msize_t);
+ARB_ARBOR_API region branch(msize_t);
 
 // Region with all segments with segment tag id.
-region tagged(int id);
+ARB_ARBOR_API region tagged(int id);
 
 // Region corresponding to a single segment.
-region segment(int id);
+ARB_ARBOR_API region segment(int id);
 
 // Region up to `distance` distal from points in `start`.
-region distal_interval(locset start, double distance);
+ARB_ARBOR_API region distal_interval(locset start, double distance);
 
 // Region up to `distance` proximal from points in `start`.
-region proximal_interval(locset end, double distance);
+ARB_ARBOR_API region proximal_interval(locset end, double distance);
 
 // Region with all segments with radius less than/less than or equal to r
-region radius_lt(region reg, double r);
-region radius_le(region reg, double r);
+ARB_ARBOR_API region radius_lt(region reg, double r);
+ARB_ARBOR_API region radius_le(region reg, double r);
 
 // Region with all segments with radius greater than/greater than or equal to r
-region radius_gt(region reg, double r);
-region radius_ge(region reg, double r);
+ARB_ARBOR_API region radius_gt(region reg, double r);
+ARB_ARBOR_API region radius_ge(region reg, double r);
 
 // Region with all segments with projection less than/less than or equal to r
-region z_dist_from_root_lt(double r);
-region z_dist_from_root_le(double r);
+ARB_ARBOR_API region z_dist_from_root_lt(double r);
+ARB_ARBOR_API region z_dist_from_root_le(double r);
 
 // Region with all segments with projection greater than/greater than or equal to r
-region z_dist_from_root_gt(double r);
-region z_dist_from_root_ge(double r);
+ARB_ARBOR_API region z_dist_from_root_gt(double r);
+ARB_ARBOR_API region z_dist_from_root_ge(double r);
 
 // Region with all segments in a cell.
-region all();
+ARB_ARBOR_API region all();
 
 // Region including all covers of included fork points.
 // (Pre-image of projection onto the topological tree.)
-region complete(region);
+ARB_ARBOR_API region complete(region);
 
 // Region associated with a name.
-region named(std::string);
+ARB_ARBOR_API region named(std::string);
 
 } // namespace reg
 
 // Union of two regions.
-region join(region, region);
+ARB_ARBOR_API region join(region, region);
 
 // Intersection of two regions.
-region intersect(region, region);
+ARB_ARBOR_API region intersect(region, region);
 
 // Closed complement of a region.
-region complement(region);
+ARB_ARBOR_API region complement(region);
 
 // (Closure of) set difference of two regions.
-region difference(region a, region b);
+ARB_ARBOR_API region difference(region a, region b);
 
 } // namespace arb
diff --git a/arbor/include/arbor/morph/segment_tree.hpp b/arbor/include/arbor/morph/segment_tree.hpp
index 4ce5a19ce2f92e946cc42f2dfb6e18b10c56399d..e934f102eb1d709e3fbb3c20136e0658e55fba29 100644
--- a/arbor/include/arbor/morph/segment_tree.hpp
+++ b/arbor/include/arbor/morph/segment_tree.hpp
@@ -5,12 +5,13 @@
 #include <vector>
 #include <string>
 
+#include <arbor/export.hpp>
 #include <arbor/morph/primitives.hpp>
 
 namespace arb {
 
 /// Morphology composed of segments.
-class segment_tree {
+class ARB_ARBOR_API segment_tree {
     struct child_prop {
         int count;
         bool is_fork() const { return count>1; }
diff --git a/arbor/include/arbor/morph/stitch.hpp b/arbor/include/arbor/morph/stitch.hpp
index d5086642a10dce1fc8a9ccfc31d7df2cc05cb532..c64b46e1de3eb61b7e398c591cf319a19c775120 100644
--- a/arbor/include/arbor/morph/stitch.hpp
+++ b/arbor/include/arbor/morph/stitch.hpp
@@ -6,6 +6,7 @@
 #include <string>
 #include <vector>
 
+#include <arbor/export.hpp>
 #include <arbor/morph/morphology.hpp>
 #include <arbor/morph/primitives.hpp>
 #include <arbor/morph/label_dict.hpp>
@@ -42,7 +43,7 @@ struct mstitch {
 struct stitch_builder_impl;
 struct stitched_morphology;
 
-struct stitch_builder {
+struct ARB_ARBOR_API stitch_builder {
     stitch_builder();
 
     stitch_builder(const stitch_builder&) = delete;
@@ -71,7 +72,7 @@ private:
 
 struct stitched_morphology_impl;
 
-struct stitched_morphology {
+struct ARB_ARBOR_API stitched_morphology {
     stitched_morphology() = delete;
     stitched_morphology(const stitch_builder&); // implicit
     stitched_morphology(stitch_builder&&); // implicit
diff --git a/arbor/include/arbor/profile/clock.hpp b/arbor/include/arbor/profile/clock.hpp
index c4e9c5c9ca8a3e32dc27ac7303f74a65557db3ef..f7183673ac342ad9233678998a137dcd797e5a6b 100644
--- a/arbor/include/arbor/profile/clock.hpp
+++ b/arbor/include/arbor/profile/clock.hpp
@@ -1,5 +1,7 @@
 #pragma once
 
+#include <arbor/export.hpp>
+
 typedef unsigned long long tick_type;
 
 // Assuming POSIX monotonic clock is available; add
@@ -9,7 +11,7 @@ typedef unsigned long long tick_type;
 namespace arb {
 namespace profile {
 
-tick_type posix_clock_gettime_monotonic_ns();
+ARB_ARBOR_API tick_type posix_clock_gettime_monotonic_ns();
 
 struct posix_clock_monotonic {
     static constexpr double seconds_per_tick() { return 1.e-9; }
diff --git a/arbor/include/arbor/profile/meter_manager.hpp b/arbor/include/arbor/profile/meter_manager.hpp
index 9459023f2ae492567bba256f83a5eb5a6428f2d4..985e43c6e80c831fdfeacd99f3162a3a7aee200a 100644
--- a/arbor/include/arbor/profile/meter_manager.hpp
+++ b/arbor/include/arbor/profile/meter_manager.hpp
@@ -4,6 +4,7 @@
 #include <string>
 #include <vector>
 
+#include <arbor/export.hpp>
 #include <arbor/context.hpp>
 #include <arbor/profile/meter.hpp>
 #include <arbor/profile/timer.hpp>
@@ -28,7 +29,7 @@ struct measurement {
     measurement(std::string, std::string, const std::vector<double>&, const context&);
 };
 
-class meter_manager {
+class ARB_ARBOR_API meter_manager {
 private:
     bool started_ = false;
 
@@ -58,8 +59,8 @@ struct meter_report {
     std::vector<std::string> hosts;
 };
 
-meter_report make_meter_report(const meter_manager& manager, const context& ctx);
-std::ostream& operator<<(std::ostream& o, const meter_report& report);
+ARB_ARBOR_API meter_report make_meter_report(const meter_manager& manager, const context& ctx);
+ARB_ARBOR_API std::ostream& operator<<(std::ostream& o, const meter_report& report);
 
 } // namespace profile
 } // namespace arb
diff --git a/arbor/include/arbor/profile/profiler.hpp b/arbor/include/arbor/profile/profiler.hpp
index f812d13d82895d3303348ffa0107a5adcde536d9..8d7d02c7b59cde44ed2a350d8231e4c3cf029f2d 100644
--- a/arbor/include/arbor/profile/profiler.hpp
+++ b/arbor/include/arbor/profile/profiler.hpp
@@ -5,6 +5,7 @@
 #include <unordered_map>
 #include <vector>
 
+#include <arbor/export.hpp>
 #include <arbor/context.hpp>
 #include <arbor/profile/timer.hpp>
 
@@ -33,15 +34,16 @@ struct profile {
     double wall_time;
 };
 
+// TODO: remove declaration and update the docs
 void profiler_clear();
-void profiler_initialize(context& ctx);
-void profiler_enter(std::size_t region_id);
-void profiler_leave();
+ARB_ARBOR_API void profiler_initialize(context& ctx);
+ARB_ARBOR_API void profiler_enter(std::size_t region_id);
+ARB_ARBOR_API void profiler_leave();
 
-profile profiler_summary();
-std::size_t profiler_region_id(const std::string& name);
+ARB_ARBOR_API profile profiler_summary();
+ARB_ARBOR_API std::size_t profiler_region_id(const std::string& name);
 
-std::ostream& operator<<(std::ostream&, const profile&);
+ARB_ARBOR_API std::ostream& operator<<(std::ostream&, const profile&);
 
 } // namespace profile
 } // namespace arb
diff --git a/arbor/include/arbor/recipe.hpp b/arbor/include/arbor/recipe.hpp
index 2853d4d0b81b788c96b1185cb23f9b9c298e2d66..ed5e621850c9f9de547056278568741db21db822 100644
--- a/arbor/include/arbor/recipe.hpp
+++ b/arbor/include/arbor/recipe.hpp
@@ -4,6 +4,7 @@
 #include <utility>
 #include <vector>
 
+#include <arbor/export.hpp>
 #include <arbor/common_types.hpp>
 #include <arbor/event_generator.hpp>
 #include <arbor/util/unique_any.hpp>
@@ -60,7 +61,7 @@ struct gap_junction_connection {
         peer(std::move(peer)), local(std::move(local)), weight(g) {}
 };
 
-class recipe {
+class ARB_ARBOR_API recipe {
 public:
     virtual cell_size_type num_cells() const = 0;
 
diff --git a/arbor/include/arbor/s_expr.hpp b/arbor/include/arbor/s_expr.hpp
index e6ac0d5156f279605dc833e4265ca602b47afc37..e8ddb218eb9c96136fcc1412247e748ab4a585f9 100644
--- a/arbor/include/arbor/s_expr.hpp
+++ b/arbor/include/arbor/s_expr.hpp
@@ -11,6 +11,8 @@
 #include <variant>
 #include <vector>
 
+#include <arbor/export.hpp>
+
 namespace arb {
 
 struct src_location {
@@ -24,7 +26,7 @@ struct src_location {
     {}
 };
 
-std::ostream& operator<<(std::ostream& o, const src_location& l);
+ARB_ARBOR_API std::ostream& operator<<(std::ostream& o, const src_location& l);
 
 enum class tok {
     nil,
@@ -38,7 +40,7 @@ enum class tok {
     error       // special error state marker
 };
 
-std::ostream& operator<<(std::ostream&, const tok&);
+ARB_ARBOR_API std::ostream& operator<<(std::ostream&, const tok&);
 
 struct token {
     src_location loc;
@@ -46,7 +48,7 @@ struct token {
     std::string spelling;
 };
 
-std::ostream& operator<<(std::ostream&, const token&);
+ARB_ARBOR_API std::ostream& operator<<(std::ostream&, const token&);
 
 struct symbol {
     std::string str;
@@ -57,7 +59,7 @@ inline symbol operator"" _symbol(const char* chars, size_t size) {
     return {chars};
 }
 
-struct s_expr {
+struct ARB_ARBOR_API s_expr {
     template <typename U>
     struct s_pair {
         U head = U();
@@ -242,16 +244,16 @@ struct s_expr {
     const_iterator cbegin() const { return {*this}; }
     const_iterator cend()   const { return const_iterator::sentinel{}; }
 
-    friend std::ostream& operator<<(std::ostream& o, const s_expr& x);
+    ARB_ARBOR_API friend std::ostream& operator<<(std::ostream& o, const s_expr& x);
 };
 
 // Build s-expr from string
-s_expr parse_s_expr(const std::string& line);
+ARB_ARBOR_API s_expr parse_s_expr(const std::string& line);
 
 // Length of the s-expr
-std::size_t length(const s_expr& l);
+ARB_ARBOR_API std::size_t length(const s_expr& l);
 
 // Location of the head of the s-expr
-src_location location(const s_expr& l);
+ARB_ARBOR_API src_location location(const s_expr& l);
 } // namespace arb
 
diff --git a/arbor/include/arbor/schedule.hpp b/arbor/include/arbor/schedule.hpp
index e75903afe268595f2458ed43caf54a706f1a3282..253efa7bf7bcc308452216248e9a3831f3da8c48 100644
--- a/arbor/include/arbor/schedule.hpp
+++ b/arbor/include/arbor/schedule.hpp
@@ -11,6 +11,7 @@
 #include <arbor/assert.hpp>
 #include <arbor/common_types.hpp>
 #include <arbor/util/extra_traits.hpp>
+#include <arbor/export.hpp>
 
 // Time schedules for probe–sampler associations.
 
@@ -103,7 +104,7 @@ inline schedule::schedule(): schedule(empty_schedule{}) {}
 // Common schedules
 
 // Schedule at k·dt for integral k≥0 within the interval [t0, t1).
-class regular_schedule_impl {
+class ARB_ARBOR_API regular_schedule_impl {
 public:
     explicit regular_schedule_impl(time_type t0, time_type dt, time_type t1):
         t0_(t0), t1_(t1), dt_(dt), oodt_(1./dt)
@@ -135,7 +136,7 @@ inline schedule regular_schedule(time_type dt) {
 
 
 // Schedule at times given explicitly via a provided sorted sequence.
-class explicit_schedule_impl {
+class ARB_ARBOR_API explicit_schedule_impl {
 public:
     explicit_schedule_impl(const explicit_schedule_impl&) = default;
     explicit_schedule_impl(explicit_schedule_impl&&) = default;
diff --git a/arbor/include/arbor/simulation.hpp b/arbor/include/arbor/simulation.hpp
index 1f4afc6926803bb516e54f1af564854096dd74cb..0559f40f257cd3032e67ea17996e00f98c55e01d 100644
--- a/arbor/include/arbor/simulation.hpp
+++ b/arbor/include/arbor/simulation.hpp
@@ -5,6 +5,7 @@
 #include <unordered_map>
 #include <vector>
 
+#include <arbor/export.hpp>
 #include <arbor/common_types.hpp>
 #include <arbor/context.hpp>
 #include <arbor/domain_decomposition.hpp>
@@ -21,7 +22,7 @@ using spike_export_function = std::function<void(const std::vector<spike>&)>;
 // simulation_state comprises private implementation for simulation class.
 class simulation_state;
 
-class simulation {
+class ARB_ARBOR_API simulation {
 public:
     simulation(const recipe& rec, const domain_decomposition& decomp, const context& ctx);
 
diff --git a/arbor/include/arbor/spike_event.hpp b/arbor/include/arbor/spike_event.hpp
index 102f3c42403dc1402f5b1cfda2e0a2b399219c30..405a357ba4e5bdcbf0f5bf0de1777232ba60fd66 100644
--- a/arbor/include/arbor/spike_event.hpp
+++ b/arbor/include/arbor/spike_event.hpp
@@ -4,6 +4,7 @@
 #include <tuple>
 #include <vector>
 
+#include <arbor/export.hpp>
 #include <arbor/common_types.hpp>
 
 namespace arb {
@@ -33,6 +34,6 @@ struct cell_spike_events {
 
 using cse_vector = std::vector<cell_spike_events>;
 
-std::ostream& operator<<(std::ostream&, const spike_event&);
+ARB_ARBOR_API std::ostream& operator<<(std::ostream&, const spike_event&);
 
 } // namespace arb
diff --git a/arbor/include/arbor/spike_source_cell.hpp b/arbor/include/arbor/spike_source_cell.hpp
index 9d04282730a1b3c888ce5b2a986c383d7c6f8f7e..5688b9949dd76016cbac40a6f25716efe680add7 100644
--- a/arbor/include/arbor/spike_source_cell.hpp
+++ b/arbor/include/arbor/spike_source_cell.hpp
@@ -1,6 +1,7 @@
 #pragma once
 
 #include <arbor/common_types.hpp>
+#include <arbor/export.hpp>
 #include <arbor/schedule.hpp>
 
 namespace arb {
@@ -8,7 +9,7 @@ namespace arb {
 // Cell description returned by recipe::cell_description(gid) for cells with
 // recipe::cell_kind(gid) returning cell_kind::spike_source
 
-struct spike_source_cell {
+struct ARB_SYMBOL_VISIBLE spike_source_cell {
     cell_tag_type source; // Label of source.
     schedule seq;
 
diff --git a/arbor/include/arbor/symmetric_recipe.hpp b/arbor/include/arbor/symmetric_recipe.hpp
index 7fdf873221a1e2bf9af162b210ad18e44a416395..57012d063ca18d6f25fbc6bb9e78196e0488687a 100644
--- a/arbor/include/arbor/symmetric_recipe.hpp
+++ b/arbor/include/arbor/symmetric_recipe.hpp
@@ -2,6 +2,7 @@
 
 #include <any>
 
+#include <arbor/export.hpp>
 #include <arbor/recipe.hpp>
 #include <arbor/util/unique_any.hpp>
 
@@ -21,7 +22,7 @@ public:
 // as many ranks as tile indicates. Its functions call the
 // underlying functions of tile and perform transformations
 // on the results when needed.
-class symmetric_recipe: public recipe {
+class ARB_ARBOR_API symmetric_recipe: public recipe {
 public:
     symmetric_recipe(std::unique_ptr<tile> rec): tiled_recipe_(std::move(rec)) {}
 
diff --git a/arbor/include/arbor/util/any_ptr.hpp b/arbor/include/arbor/util/any_ptr.hpp
index f148c566bbd5455d32dd5158c3a6a7942060d09b..c6e11b823c95ea622bd3bae5e858641175cc700a 100644
--- a/arbor/include/arbor/util/any_ptr.hpp
+++ b/arbor/include/arbor/util/any_ptr.hpp
@@ -29,13 +29,14 @@
 #include <cstddef>
 #include <type_traits>
 
+#include <arbor/export.hpp>
 #include <arbor/util/any_cast.hpp>
 #include <arbor/util/lexcmp_def.hpp>
 
 namespace arb {
 namespace util {
 
-struct any_ptr {
+struct ARB_SYMBOL_VISIBLE any_ptr {
     any_ptr() {}
 
     any_ptr(std::nullptr_t) {}
diff --git a/arbor/include/arbor/util/unique_any.hpp b/arbor/include/arbor/util/unique_any.hpp
index 3b4ebffce308929d152ffcbb2df59bf387a6735e..9cbcf67d14077f9dfb3fac0a56052de2afd5a75f 100644
--- a/arbor/include/arbor/util/unique_any.hpp
+++ b/arbor/include/arbor/util/unique_any.hpp
@@ -5,6 +5,7 @@
 #include <typeinfo>
 #include <type_traits>
 
+#include <arbor/export.hpp>
 #include <arbor/util/any_cast.hpp>
 #include <arbor/util/extra_traits.hpp>
 
@@ -44,7 +45,7 @@
 namespace arb {
 namespace util {
 
-class unique_any {
+class ARB_SYMBOL_VISIBLE unique_any {
 public:
     constexpr unique_any() = default;
 
diff --git a/arbor/include/arbor/util/visibility.hpp b/arbor/include/arbor/util/visibility.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..b818091cb41783b5a22af9b5907e3470136c1210
--- /dev/null
+++ b/arbor/include/arbor/util/visibility.hpp
@@ -0,0 +1,51 @@
+#pragma once
+
+#if defined(__INTEL_COMPILER) || defined(__ICL) || defined(__ICC) || defined(__ECC)
+//  Intel
+#   if defined(_WIN32) || defined(__WIN32__) || defined(WIN32) || defined(__CYGWIN__)
+#       define ARB_SYMBOL_IMPORT __attribute__((__dllimport__))
+#       define ARB_SYMBOL_EXPORT __attribute__((__dllexport__))
+#   elif defined(__GNUC__) && (__GNUC__ >= 4)
+#       define ARB_SYMBOL_EXPORT __attribute__((visibility("default")))
+#       define ARB_SYMBOL_VISIBLE __attribute__((__visibility__("default")))
+#   endif
+
+#elif defined(__clang__)
+//  Clang C++
+#   if defined(_WIN32) || defined(__WIN32__) || defined(WIN32) || defined(__CYGWIN__)
+#       define ARB_SYMBOL_IMPORT __attribute__((__dllimport__))
+#       define ARB_SYMBOL_EXPORT __attribute__((__dllexport__))
+#   else
+#       define ARB_SYMBOL_EXPORT __attribute__((__visibility__("default")))
+#       define ARB_SYMBOL_VISIBLE __attribute__((__visibility__("default")))
+#   endif
+
+# elif defined(__GNUC__)
+//  GNU C++:
+#   if __GNUC__ >= 4
+#       if defined(_WIN32) || defined(__WIN32__) || defined(WIN32) || defined(__CYGWIN__)
+#           define ARB_SYMBOL_IMPORT __attribute__((__dllimport__))
+#           define ARB_SYMBOL_EXPORT __attribute__((__dllexport__))
+#       else
+#           define ARB_SYMBOL_EXPORT __attribute__((__visibility__("default")))
+#           define ARB_SYMBOL_VISIBLE __attribute__((__visibility__("default")))
+#       endif
+#   endif
+
+#endif
+
+#if defined(macintosh) || defined(__APPLE__) || defined(__APPLE_CC__)
+// MacOS
+#   define ARB_ON_MACOS
+#endif
+
+#ifndef ARB_SYMBOL_IMPORT
+#   define ARB_SYMBOL_IMPORT
+#endif
+#ifndef ARB_SYMBOL_EXPORT
+#   define ARB_SYMBOL_EXPORT
+#endif
+#ifndef ARB_SYMBOL_VISIBLE
+#   define ARB_SYMBOL_VISIBLE
+#endif
+
diff --git a/arbor/include/git-source-id b/arbor/include/git-source-id
index c3fc3ec9e4e793f3b42775f89be2da8c17f948c1..f2cc49cbe4c1b61c1fd9b382a3a0aaae15bac31c 100755
--- a/arbor/include/git-source-id
+++ b/arbor/include/git-source-id
@@ -44,16 +44,18 @@ done
 cat << __end__
 #pragma once
 
+#include <arbor/export.hpp>
+
 namespace arb {
-extern const char* source_id;
-extern const char* arch;
-extern const char* build_config;
-extern const char* version;
-extern const char* full_build_id;
+ARB_ARBOR_API extern const char* source_id;
+ARB_ARBOR_API extern const char* arch;
+ARB_ARBOR_API extern const char* build_config;
+ARB_ARBOR_API extern const char* version;
+ARB_ARBOR_API extern const char* full_build_id;
 constexpr int version_major = ${version_major};
 constexpr int version_minor = ${version_minor};
 constexpr int version_patch = ${version_patch};
-extern const char* version_dev;
+ARB_ARBOR_API extern const char* version_dev;
 }
 
 #define ARB_SOURCE_ID "${gitlog}"
diff --git a/arbor/label_resolution.hpp b/arbor/label_resolution.hpp
index 5912672e13e2642c98693cdf7fe73978df3cb89c..7cd69a5bd8cbc8908cb9293fb3bf6d9b2ad17ea4 100644
--- a/arbor/label_resolution.hpp
+++ b/arbor/label_resolution.hpp
@@ -3,6 +3,7 @@
 #include <unordered_map>
 #include <vector>
 
+#include <arbor/export.hpp>
 #include <arbor/arbexcept.hpp>
 #include <arbor/common_types.hpp>
 #include <arbor/util/expected.hpp>
@@ -17,7 +18,7 @@ using lid_hopefully = arb::util::expected<cell_lid_type, std::string>;
 // `sizes` is a partitioning vector for associating a cell with a set of
 // (label, range) pairs in `labels`, `ranges`.
 // gids of the cells are unknown.
-class cell_label_range {
+class ARB_ARBOR_API cell_label_range {
 public:
     cell_label_range() = default;
     cell_label_range(cell_label_range&&) = default;
@@ -52,7 +53,7 @@ private:
 };
 
 // Struct for associating each cell of `cell_label_range` with a gid.
-struct cell_labels_and_gids {
+struct ARB_ARBOR_API cell_labels_and_gids {
     cell_labels_and_gids() = default;
     cell_labels_and_gids(cell_label_range lr, std::vector<cell_gid_type> gids);
 
@@ -67,7 +68,7 @@ struct cell_labels_and_gids {
 // Class constructed from `cell_labels_and_ranges`:
 // Represents the information in the object in a more
 // structured manner for lid resolution in `resolver`
-class label_resolution_map {
+class ARB_ARBOR_API label_resolution_map {
 public:
     struct range_set {
         std::vector<lid_range> ranges;
@@ -86,20 +87,20 @@ private:
     std::unordered_map<cell_gid_type, std::unordered_map<cell_tag_type, range_set>> map;
 };
 
-struct round_robin_state {
+struct ARB_ARBOR_API round_robin_state {
     cell_size_type state = 0;
     round_robin_state() : state(0) {};
     round_robin_state(cell_lid_type state) : state(state) {};
     lid_hopefully update(const label_resolution_map::range_set& range);
 };
 
-struct assert_univalent_state {
+struct ARB_ARBOR_API assert_univalent_state {
     lid_hopefully update(const label_resolution_map::range_set& range);
 };
 
 // Struct used for resolving the lid of a (gid, label, lid_selection_policy) input.
 // Requires a `label_resolution_map` which stores the constant mapping of (gid, label) pairs to lid sets.
-struct resolver {
+struct ARB_ARBOR_API resolver {
     resolver(const label_resolution_map* label_map): label_map_(label_map) {}
     cell_lid_type resolve(const cell_global_label_type& iden);
 
diff --git a/arbor/lif_cell_group.hpp b/arbor/lif_cell_group.hpp
index feda74c97808f5fcbb90eca20b959bf30406da7e..b89e442f863206f13f5b5b1c25e2b293f8044be8 100644
--- a/arbor/lif_cell_group.hpp
+++ b/arbor/lif_cell_group.hpp
@@ -2,6 +2,7 @@
 
 #include <vector>
 
+#include <arbor/export.hpp>
 #include <arbor/common_types.hpp>
 #include <arbor/lif_cell.hpp>
 #include <arbor/recipe.hpp>
@@ -13,7 +14,7 @@
 
 namespace arb {
 
-class lif_cell_group: public cell_group {
+class ARB_ARBOR_API lif_cell_group: public cell_group {
 public:
     using value_type = double;
 
diff --git a/arbor/mc_cell_group.hpp b/arbor/mc_cell_group.hpp
index 173aae9ae6681cfd4d08b90979d52de88a51595b..3640c8cf248a66e1e43f790665f1b11bd9ddbe77 100644
--- a/arbor/mc_cell_group.hpp
+++ b/arbor/mc_cell_group.hpp
@@ -7,6 +7,7 @@
 #include <unordered_map>
 #include <vector>
 
+#include <arbor/export.hpp>
 #include <arbor/common_types.hpp>
 #include <arbor/recipe.hpp>
 #include <arbor/sampling.hpp>
@@ -23,7 +24,7 @@
 
 namespace arb {
 
-class mc_cell_group: public cell_group {
+class ARB_ARBOR_API mc_cell_group: public cell_group {
 public:
     mc_cell_group() = default;
 
diff --git a/arbor/mechcat.cpp b/arbor/mechcat.cpp
index 3dc187742e5c5a5c1e04ef63ef90a7e07528f573..161c5f9a58253d770e65f8db721d08ac66de02eb 100644
--- a/arbor/mechcat.cpp
+++ b/arbor/mechcat.cpp
@@ -592,7 +592,7 @@ std::pair<mechanism_ptr, mechanism_overrides> mechanism_catalogue::instance_impl
 
 mechanism_catalogue::~mechanism_catalogue() = default;
 
-const mechanism_catalogue& load_catalogue(const std::string& fn) {
+ARB_ARBOR_API const mechanism_catalogue& load_catalogue(const std::string& fn) {
     typedef const void* global_catalogue_t();
     global_catalogue_t* get_catalogue = nullptr;
     try {
diff --git a/arbor/merge_events.hpp b/arbor/merge_events.hpp
index 9a8581f62e4d766fb9a222773fbbb8f7d951bdea..9ce69241eac9a9559af69df9ab6ff8359c86f440 100644
--- a/arbor/merge_events.hpp
+++ b/arbor/merge_events.hpp
@@ -3,6 +3,7 @@
 #include <iosfwd>
 #include <vector>
 
+#include <arbor/export.hpp>
 #include <arbor/event_generator.hpp>
 #include <arbor/spike_event.hpp>
 
@@ -21,7 +22,7 @@ namespace impl {
     // The tournament tree is used internally by the merge_events method, and
     // it is not intended for use elsewhere. It is exposed here for unit testing
     // of its functionality.
-    class tourney_tree {
+    class ARB_ARBOR_API tourney_tree {
         using key_val = std::pair<unsigned, spike_event>;
 
     public:
diff --git a/arbor/morph/cv_data.cpp b/arbor/morph/cv_data.cpp
index 410bac0cc4bc1d1db0f2b6936f4ea3d9563b1925..719d609ab44566bed0c6e6b16d8bd304ba71afbc 100644
--- a/arbor/morph/cv_data.cpp
+++ b/arbor/morph/cv_data.cpp
@@ -137,7 +137,7 @@ fvm_size_type cell_cv_data::size() const {
     return impl_->cv_parent.size();
 }
 
-std::optional<cell_cv_data> cv_data(const cable_cell& cell) {
+ARB_ARBOR_API std::optional<cell_cv_data> cv_data(const cable_cell& cell) {
     if (auto policy = cell.decorations().defaults().discretization) {
         return cell_cv_data(cell, policy->cv_boundary_points(cell));
     }
@@ -154,7 +154,7 @@ cell_cv_data::cell_cv_data(const cable_cell& cell, const locset& lset):
     provider_(cell.provider())
 {}
 
-std::vector<cv_proportion> intersect_region(const region& reg, const cell_cv_data& geom, bool by_length) {
+ARB_ARBOR_API std::vector<cv_proportion> intersect_region(const region& reg, const cell_cv_data& geom, bool by_length) {
     const auto& mp = geom.provider();
     const auto& embedding = mp.embedding();
 
diff --git a/arbor/morph/locset.cpp b/arbor/morph/locset.cpp
index 0cb46fe2d95ffe8a088d53f57bb9ea14e6e3a5b5..3502caa0086ac8567511e1711a9d8cef6a47fe8f 100644
--- a/arbor/morph/locset.cpp
+++ b/arbor/morph/locset.cpp
@@ -33,7 +33,7 @@ void assert_valid(mlocation x) {
 
 struct nil_: locset_tag {};
 
-locset nil() {
+ARB_ARBOR_API locset nil() {
     return locset{nil_{}};
 }
 
@@ -52,7 +52,7 @@ struct location_: locset_tag {
     mlocation loc;
 };
 
-locset location(msize_t branch, double pos) {
+ARB_ARBOR_API locset location(msize_t branch, double pos) {
     mlocation loc{branch, pos};
     assert_valid(loc);
     return locset{location_{loc}};
@@ -101,7 +101,7 @@ std::ostream& operator<<(std::ostream& o, const location_list_& x) {
 
 struct terminal_: locset_tag {};
 
-locset terminal() {
+ARB_ARBOR_API locset terminal() {
     return locset{terminal_{}};
 }
 
@@ -162,7 +162,7 @@ mlocation_list thingify_(const proximal_translate_& dt, const mprovider& p) {
     return L;
 }
 
-locset proximal_translate(locset ls, double distance) {
+ARB_ARBOR_API locset proximal_translate(locset ls, double distance) {
     return locset(proximal_translate_{ls, distance});
 }
 
@@ -177,7 +177,7 @@ struct distal_translate_: locset_tag {
     double distance;
 };
 
-locset distal_translate(locset ls, double distance) {
+ARB_ARBOR_API locset distal_translate(locset ls, double distance) {
     return locset(distal_translate_{ls, distance});
 }
 
@@ -253,7 +253,7 @@ std::ostream& operator<<(std::ostream& o, const distal_translate_& l) {
 
 struct root_: locset_tag {};
 
-locset root() {
+ARB_ARBOR_API locset root() {
     return locset{root_{}};
 }
 
@@ -269,7 +269,7 @@ std::ostream& operator<<(std::ostream& o, const root_& x) {
 
 struct segments_: locset_tag {};
 
-locset segment_boundaries() {
+ARB_ARBOR_API locset segment_boundaries() {
     return locset{segments_{}};
 }
 
@@ -289,7 +289,7 @@ struct on_branches_: locset_tag {
     double pos;
 };
 
-locset on_branches(double pos) {
+ARB_ARBOR_API locset on_branches(double pos) {
     return locset{on_branches_{pos}};
 }
 
@@ -315,7 +315,7 @@ struct named_: locset_tag {
     std::string name;
 };
 
-locset named(std::string name) {
+ARB_ARBOR_API locset named(std::string name) {
     return locset(named_{std::move(name)});
 }
 
@@ -334,7 +334,7 @@ struct most_distal_: locset_tag {
     region reg;
 };
 
-locset most_distal(region reg) {
+ARB_ARBOR_API locset most_distal(region reg) {
     return locset(most_distal_{std::move(reg)});
 }
 
@@ -358,7 +358,7 @@ struct most_proximal_: locset_tag {
     region reg;
 };
 
-locset most_proximal(region reg) {
+ARB_ARBOR_API locset most_proximal(region reg) {
     return locset(most_proximal_{std::move(reg)});
 }
 
@@ -386,7 +386,7 @@ struct boundary_: locset_tag {
     region reg;
 };
 
-locset boundary(region reg) {
+ARB_ARBOR_API locset boundary(region reg) {
     return locset(boundary_(std::move(reg)));
 };
 
@@ -422,7 +422,7 @@ struct cboundary_: locset_tag {
     region reg;
 };
 
-locset cboundary(region reg) {
+ARB_ARBOR_API locset cboundary(region reg) {
     return locset(cboundary_(std::move(reg)));
 };
 
@@ -462,7 +462,7 @@ struct on_components_: locset_tag {
     region reg;
 };
 
-locset on_components(double relpos, region reg) {
+ARB_ARBOR_API locset on_components(double relpos, region reg) {
     return locset(on_components_(relpos, std::move(reg)));
 }
 
@@ -541,7 +541,7 @@ struct uniform_: locset_tag {
     uint64_t seed;
 };
 
-locset uniform(arb::region reg, unsigned left, unsigned right, uint64_t seed) {
+ARB_ARBOR_API locset uniform(arb::region reg, unsigned left, unsigned right, uint64_t seed) {
     return locset(uniform_{reg, left, right, seed});
 }
 
@@ -647,7 +647,7 @@ struct lsup_: locset_tag {
     lsup_(locset arg): arg(std::move(arg)) {}
 };
 
-locset support(locset arg) {
+ARB_ARBOR_API locset support(locset arg) {
     return locset{lsup_{std::move(arg)}};
 }
 
@@ -686,7 +686,7 @@ mlocation_list thingify_(const lrestrict_& P, const mprovider& p) {
     return L;
 }
 
-locset restrict(locset ls, region reg) {
+ARB_ARBOR_API locset restrict(locset ls, region reg) {
     return locset{lrestrict_{std::move(ls), std::move(reg)}};
 }
 
@@ -704,11 +704,11 @@ locset intersect(locset lhs, locset rhs) {
     return locset(ls::land(std::move(lhs), std::move(rhs)));
 }
 
-locset join(locset lhs, locset rhs) {
+ARB_ARBOR_API locset join(locset lhs, locset rhs) {
     return locset(ls::lor(std::move(lhs), std::move(rhs)));
 }
 
-locset sum(locset lhs, locset rhs) {
+ARB_ARBOR_API locset sum(locset lhs, locset rhs) {
     return locset(ls::lsum(std::move(lhs), std::move(rhs)));
 }
 
diff --git a/arbor/morph/morphology.cpp b/arbor/morph/morphology.cpp
index ff126d7372fa91c771a6412041979722c7f3f6a9..34159271493ec2920ca64c836694b2a18cbcf447 100644
--- a/arbor/morph/morphology.cpp
+++ b/arbor/morph/morphology.cpp
@@ -173,13 +173,13 @@ msize_t morphology::num_branches() const {
     return impl_->branches_.size();
 }
 
-std::ostream& operator<<(std::ostream& o, const morphology& m) {
+ARB_ARBOR_API std::ostream& operator<<(std::ostream& o, const morphology& m) {
     return o << *m.impl_;
 }
 
 // Utilities.
 
-mlocation_list minset(const morphology& m, const mlocation_list& in) {
+ARB_ARBOR_API mlocation_list minset(const morphology& m, const mlocation_list& in) {
     mlocation_list L;
 
     std::stack<msize_t> stack;
@@ -213,7 +213,7 @@ mlocation_list minset(const morphology& m, const mlocation_list& in) {
     return L;
 }
 
-mlocation_list maxset(const morphology& m, const mlocation_list& in_) {
+ARB_ARBOR_API mlocation_list maxset(const morphology& m, const mlocation_list& in_) {
     mlocation_list L;
 
     // Sort the input in reverse order, so that more distal locations
@@ -244,7 +244,7 @@ mlocation_list maxset(const morphology& m, const mlocation_list& in_) {
     return L;
 }
 
-mlocation canonical(const morphology& m, mlocation loc) {
+ARB_ARBOR_API mlocation canonical(const morphology& m, mlocation loc) {
     if (loc.pos==0) {
         msize_t parent = m.branch_parent(loc.branch);
         return parent==mnpos? mlocation{0, 0.}: mlocation{parent, 1.};
@@ -337,7 +337,7 @@ bool mextent::intersects(const mcable_list& a) const {
     return false;
 }
 
-mextent intersect(const mextent& a, const mextent& b) {
+ARB_ARBOR_API mextent intersect(const mextent& a, const mextent& b) {
     auto precedes = [](mcable x, mcable y) {
         return x.branch<y.branch || (x.branch==y.branch && x.dist_pos<y.prox_pos);
     };
@@ -387,7 +387,7 @@ mextent join(const mextent& a, const mextent& b) {
     return m;
 }
 
-std::vector<mextent> components(const morphology& m, const mextent& ex) {
+ARB_ARBOR_API std::vector<mextent> components(const morphology& m, const mextent& ex) {
     std::unordered_map<mlocation, unsigned> component_index;
     std::vector<mcable_list> component_cables;
 
diff --git a/arbor/morph/primitives.cpp b/arbor/morph/primitives.cpp
index d4b780feccbb4b15165015e8f8c3871fe1a435f4..be9444b7e07d33d1ca0d5c3c31a611d0ffd0cc24 100644
--- a/arbor/morph/primitives.cpp
+++ b/arbor/morph/primitives.cpp
@@ -36,7 +36,7 @@ int multiplicity(T& it, T end) {
 
 
 // interpolate between two points.
-mpoint lerp(const mpoint& a, const mpoint& b, double u) {
+ARB_ARBOR_API mpoint lerp(const mpoint& a, const mpoint& b, double u) {
     return { math::lerp(a.x, b.x, u),
              math::lerp(a.y, b.y, u),
              math::lerp(a.z, b.z, u),
@@ -44,12 +44,12 @@ mpoint lerp(const mpoint& a, const mpoint& b, double u) {
 }
 
 // test if two morphology sample points share the same location.
-bool is_collocated(const mpoint& a, const mpoint& b) {
+ARB_ARBOR_API bool is_collocated(const mpoint& a, const mpoint& b) {
     return a.x==b.x && a.y==b.y && a.z==b.z;
 }
 
 // calculate the distance between two morphology sample points.
-double distance(const mpoint& a, const mpoint& b) {
+ARB_ARBOR_API double distance(const mpoint& a, const mpoint& b) {
     double dx = a.x - b.x;
     double dy = a.y - b.y;
     double dz = a.z - b.z;
@@ -57,18 +57,18 @@ double distance(const mpoint& a, const mpoint& b) {
     return std::sqrt(dx*dx + dy*dy + dz*dz);
 }
 
-bool test_invariants(const mlocation& l) {
+ARB_ARBOR_API bool test_invariants(const mlocation& l) {
     return (0.<=l.pos && l.pos<=1.) && l.branch!=mnpos;
 }
 
-mlocation_list sum(const mlocation_list& lhs, const mlocation_list& rhs) {
+ARB_ARBOR_API mlocation_list sum(const mlocation_list& lhs, const mlocation_list& rhs) {
     mlocation_list v;
     v.resize(lhs.size() + rhs.size());
     std::merge(lhs.begin(), lhs.end(), rhs.begin(), rhs.end(), v.begin());
     return v;
 }
 
-mlocation_list join(const mlocation_list& lhs, const mlocation_list& rhs) {
+ARB_ARBOR_API mlocation_list join(const mlocation_list& lhs, const mlocation_list& rhs) {
     mlocation_list L;
     L.reserve(lhs.size()+rhs.size());
 
@@ -91,7 +91,7 @@ mlocation_list join(const mlocation_list& lhs, const mlocation_list& rhs) {
     return L;
 }
 
-mlocation_list intersection(const mlocation_list& lhs, const mlocation_list& rhs) {
+ARB_ARBOR_API mlocation_list intersection(const mlocation_list& lhs, const mlocation_list& rhs) {
     mlocation_list L;
     L.reserve(lhs.size()+rhs.size());
 
@@ -118,41 +118,41 @@ mlocation_list intersection(const mlocation_list& lhs, const mlocation_list& rhs
     return L;
 }
 
-mlocation_list support(mlocation_list L) {
+ARB_ARBOR_API mlocation_list support(mlocation_list L) {
     util::unique_in_place(L);
     return L;
 }
 
-bool test_invariants(const mcable& c) {
+ARB_ARBOR_API bool test_invariants(const mcable& c) {
     return (0.<=c.prox_pos && c.prox_pos<=c.dist_pos && c.dist_pos<=1.) && c.branch!=mnpos;
 }
 
-bool test_invariants(const mcable_list& l) {
+ARB_ARBOR_API bool test_invariants(const mcable_list& l) {
     return std::is_sorted(l.begin(), l.end())
         && l.end()==std::find_if(l.begin(), l.end(), [](const mcable& c) {return !test_invariants(c);});
 }
 
-std::ostream& operator<<(std::ostream& o, const mpoint& p) {
+ARB_ARBOR_API std::ostream& operator<<(std::ostream& o, const mpoint& p) {
     return o << "(point " << p.x << " " << p.y << " " << p.z << " " << p.radius << ")";
 }
 
-std::ostream& operator<<(std::ostream& o, const msegment& s) {
+ARB_ARBOR_API std::ostream& operator<<(std::ostream& o, const msegment& s) {
     return o << "(segment " << s.id << " " << s.prox << " " << s.dist << " " << s.tag << ")";
 }
 
-std::ostream& operator<<(std::ostream& o, const mlocation& l) {
+ARB_ARBOR_API std::ostream& operator<<(std::ostream& o, const mlocation& l) {
     return o << "(location " << l.branch << " " << l.pos << ")";
 }
 
-std::ostream& operator<<(std::ostream& o, const mlocation_list& l) {
+ARB_ARBOR_API std::ostream& operator<<(std::ostream& o, const mlocation_list& l) {
     return o << "(list " << io::sepval(l, ' ') << ")";
 }
 
-std::ostream& operator<<(std::ostream& o, const mcable& c) {
+ARB_ARBOR_API std::ostream& operator<<(std::ostream& o, const mcable& c) {
     return o << "(cable " << c.branch << " " << c.prox_pos << " " << c.dist_pos << ")";
 }
 
-std::ostream& operator<<(std::ostream& o, const mcable_list& c) {
+ARB_ARBOR_API std::ostream& operator<<(std::ostream& o, const mcable_list& c) {
     return o << "(list " << io::sepval(c, ' ') << ")";
 }
 
diff --git a/arbor/morph/region.cpp b/arbor/morph/region.cpp
index 038b1364a3cdc2a395ae42597c78eebceaa28d6e..cf59e6ea255c7441a02cb6cd5f708fee1917d0a4 100644
--- a/arbor/morph/region.cpp
+++ b/arbor/morph/region.cpp
@@ -32,7 +32,7 @@ std::optional<mcable> intersect(const mcable& a, const mcable& b) {
 
 struct nil_: region_tag {};
 
-region nil() {
+ARB_ARBOR_API region nil() {
     return region{nil_{}};
 }
 
@@ -52,7 +52,7 @@ struct cable_: region_tag {
     mcable cable;
 };
 
-region cable(msize_t id, double prox, double dist) {
+ARB_ARBOR_API region cable(msize_t id, double prox, double dist) {
     mcable c{id, prox, dist};
     if (!test_invariants(c)) {
         throw invalid_mcable(c);
@@ -60,7 +60,7 @@ region cable(msize_t id, double prox, double dist) {
     return region(cable_{c});
 }
 
-region branch(msize_t bid) {
+ARB_ARBOR_API region branch(msize_t bid) {
     return cable(bid, 0, 1);
 }
 
@@ -137,7 +137,7 @@ struct tagged_: region_tag {
     int tag;
 };
 
-region tagged(int id) {
+ARB_ARBOR_API region tagged(int id) {
     return region(tagged_{id});
 }
 
@@ -170,7 +170,7 @@ struct segment_: region_tag {
     int id;
 };
 
-region segment(int id) {
+ARB_ARBOR_API region segment(int id) {
     return region(segment_{id});
 }
 
@@ -194,7 +194,7 @@ std::ostream& operator<<(std::ostream& o, const segment_& reg) {
 
 struct all_: region_tag {};
 
-region all() {
+ARB_ARBOR_API region all() {
     return region(all_{});
 }
 
@@ -220,7 +220,7 @@ struct distal_interval_: region_tag {
     double distance; //um
 };
 
-region distal_interval(locset start, double distance) {
+ARB_ARBOR_API region distal_interval(locset start, double distance) {
     return region(distal_interval_{start, distance});
 }
 
@@ -295,7 +295,7 @@ struct proximal_interval_: region_tag {
     double distance; //um
 };
 
-region proximal_interval(locset end, double distance) {
+ARB_ARBOR_API region proximal_interval(locset end, double distance) {
     return region(proximal_interval_{end, distance});
 }
 
@@ -368,7 +368,7 @@ struct radius_lt_: region_tag {
     double val; //um
 };
 
-region radius_lt(region reg, double val) {
+ARB_ARBOR_API region radius_lt(region reg, double val) {
     return region(radius_lt_{reg, val});
 }
 
@@ -387,7 +387,7 @@ struct radius_le_: region_tag {
     double val; //um
 };
 
-region radius_le(region reg, double val) {
+ARB_ARBOR_API region radius_le(region reg, double val) {
     return region(radius_le_{reg, val});
 }
 
@@ -406,7 +406,7 @@ struct radius_gt_: region_tag {
     double val; //um
 };
 
-region radius_gt(region reg, double val) {
+ARB_ARBOR_API region radius_gt(region reg, double val) {
     return region(radius_gt_{reg, val});
 }
 
@@ -425,7 +425,7 @@ struct radius_ge_: region_tag {
     double val; //um
 };
 
-region radius_ge(region reg, double val) {
+ARB_ARBOR_API region radius_ge(region reg, double val) {
     return region(radius_ge_{reg, val});
 }
 
@@ -521,7 +521,7 @@ std::ostream& operator<<(std::ostream& o, const projection_ge_& r) {
     return o << "(projection-ge " << r.val << ")";
 }
 
-region z_dist_from_root_lt(double r0) {
+ARB_ARBOR_API region z_dist_from_root_lt(double r0) {
     if (r0 == 0) {
         return {};
     }
@@ -530,19 +530,19 @@ region z_dist_from_root_lt(double r0) {
     return intersect(std::move(lt), std::move(gt));
 }
 
-region z_dist_from_root_le(double r0) {
+ARB_ARBOR_API region z_dist_from_root_le(double r0) {
     region le = reg::projection_le(r0);
     region ge = reg::projection_ge(-r0);
     return intersect(std::move(le), std::move(ge));
 }
 
-region z_dist_from_root_gt(double r0) {
+ARB_ARBOR_API region z_dist_from_root_gt(double r0) {
     region lt = reg::projection_lt(-r0);
     region gt = reg::projection_gt(r0);
     return region{join(std::move(lt), std::move(gt))};
 }
 
-region z_dist_from_root_ge(double r0) {
+ARB_ARBOR_API region z_dist_from_root_ge(double r0) {
     region lt = reg::projection_le(-r0);
     region gt = reg::projection_ge(r0);
     return region{join(std::move(lt), std::move(gt))};
@@ -554,7 +554,7 @@ struct named_: region_tag {
     std::string name;
 };
 
-region named(std::string name) {
+ARB_ARBOR_API region named(std::string name) {
     return region(named_{std::move(name)});
 }
 
@@ -573,7 +573,7 @@ struct super_: region_tag {
     region reg;
 };
 
-region complete(region r) {
+ARB_ARBOR_API region complete(region r) {
     return region(super_{std::move(r)});
 }
 
@@ -733,19 +733,19 @@ std::ostream& operator<<(std::ostream& o, const reg_minus& x) {
 // namespace with region so that ADL allows for construction of expressions
 // with regions without having to namespace qualify these operations.
 
-region intersect(region l, region r) {
+ARB_ARBOR_API region intersect(region l, region r) {
     return region{reg::reg_and(std::move(l), std::move(r))};
 }
 
-region join(region l, region r) {
+ARB_ARBOR_API region join(region l, region r) {
     return region{reg::reg_or(std::move(l), std::move(r))};
 }
 
-region complement(region r) {
+ARB_ARBOR_API region complement(region r) {
     return region{reg::reg_not(std::move(r))};
 }
 
-region difference(region l, region r) {
+ARB_ARBOR_API region difference(region l, region r) {
     return region{reg::reg_minus(std::move(l), std::move(r))};
 }
 
diff --git a/arbor/morph/segment_tree.cpp b/arbor/morph/segment_tree.cpp
index 955307cb10c793de62371847cae8f80da84fac0f..70498e47095995c6cea8166758fc5b12a4488c13 100644
--- a/arbor/morph/segment_tree.cpp
+++ b/arbor/morph/segment_tree.cpp
@@ -76,7 +76,7 @@ bool segment_tree::is_root(msize_t i) const {
     return parents_[i]==mnpos;
 }
 
-std::ostream& operator<<(std::ostream& o, const segment_tree& m) {
+ARB_ARBOR_API std::ostream& operator<<(std::ostream& o, const segment_tree& m) {
     auto tstr = util::transform_view(m.parents_,
             [](msize_t i) -> std::string {
                 return i==mnpos? "npos": std::to_string(i);
diff --git a/arbor/partition_load_balance.cpp b/arbor/partition_load_balance.cpp
index 40b145b003bb04e5bfc7e5c3117eb622f8984a4d..254bd53a6f808d2f39b0ba2b1b98fe6f4fa6d9cc 100644
--- a/arbor/partition_load_balance.cpp
+++ b/arbor/partition_load_balance.cpp
@@ -20,7 +20,7 @@
 
 namespace arb {
 
-domain_decomposition partition_load_balance(
+ARB_ARBOR_API domain_decomposition partition_load_balance(
     const recipe& rec,
     const context& ctx,
     partition_hint_map hint_map)
diff --git a/arbor/profile/clock.cpp b/arbor/profile/clock.cpp
index bd73d4aae733f46ef0a54c13a946b6928596b792..2362b2461e59fe9dafedf51b83f8884e423c4bc5 100644
--- a/arbor/profile/clock.cpp
+++ b/arbor/profile/clock.cpp
@@ -23,7 +23,7 @@ inline tick_type posix_clock_gettime_ns(clockid_t clock) {
     return nanoseconds;
 };
 
-tick_type posix_clock_gettime_monotonic_ns() {
+ARB_ARBOR_API tick_type posix_clock_gettime_monotonic_ns() {
     return posix_clock_gettime_ns(CLOCK_MONOTONIC);
 }
 
diff --git a/arbor/profile/meter_manager.cpp b/arbor/profile/meter_manager.cpp
index dc2e12804cd8fa44078dfdec04422b052164e278..c2540c84267c9e08154113a515cf8638649091e6 100644
--- a/arbor/profile/meter_manager.cpp
+++ b/arbor/profile/meter_manager.cpp
@@ -101,7 +101,7 @@ const std::vector<double>& meter_manager::times() const {
 
 // Build a report of meters, for use at the end of a simulation
 // for output to file or analysis.
-meter_report make_meter_report(const meter_manager& manager, const context& ctx) {
+ARB_ARBOR_API meter_report make_meter_report(const meter_manager& manager, const context& ctx) {
     meter_report report;
 
     // Add the times to the meter outputs
@@ -131,7 +131,7 @@ meter_report make_meter_report(const meter_manager& manager, const context& ctx)
 }
 
 // Print easy to read report of meters to a stream.
-std::ostream& operator<<(std::ostream& o, const meter_report& report) {
+ARB_ARBOR_API std::ostream& operator<<(std::ostream& o, const meter_report& report) {
     o << "\n---- meters -------------------------------------------------------------------------------\n";
     o << strprintf("meter%16s", "");
     for (auto const& m: report.meters) {
diff --git a/arbor/profile/profiler.cpp b/arbor/profile/profiler.cpp
index 03bfc75070706a37a8c6a4b0f4623e8d97240623..c5e0b641dfcb59ad2a9fe6896ff6e156d613b15e 100644
--- a/arbor/profile/profiler.cpp
+++ b/arbor/profile/profiler.cpp
@@ -342,27 +342,27 @@ void print(std::ostream& o,
 // convenience functions for instrumenting code.
 //
 
-void profiler_leave() {
+ARB_ARBOR_API void profiler_leave() {
     profiler::get_global_profiler().leave();
 }
 
-region_id_type profiler_region_id(const std::string& name) {
+ARB_ARBOR_API region_id_type profiler_region_id(const std::string& name) {
     if (!is_valid_region_string(name)) {
         throw std::runtime_error(std::string("'")+name+"' is not a valid profiler region name.");
     }
     return profiler::get_global_profiler().region_index(name);
 }
 
-void profiler_enter(region_id_type region_id) {
+ARB_ARBOR_API void profiler_enter(region_id_type region_id) {
     profiler::get_global_profiler().enter(region_id);
 }
 
-void profiler_initialize(context& ctx) {
+ARB_ARBOR_API void profiler_initialize(context& ctx) {
     profiler::get_global_profiler().initialize(ctx->thread_pool);
 }
 
 // Print profiler statistics to an ostream
-std::ostream& operator<<(std::ostream& o, const profile& prof) {
+ARB_ARBOR_API std::ostream& operator<<(std::ostream& o, const profile& prof) {
     char buf[80];
 
     auto tree = make_profile_tree(prof);
@@ -373,19 +373,18 @@ std::ostream& operator<<(std::ostream& o, const profile& prof) {
     return o;
 }
 
-profile profiler_summary() {
+ARB_ARBOR_API profile profiler_summary() {
     return profiler::get_global_profiler().results();
 }
 
 #else
 
-void profiler_leave() {}
-void profiler_enter(region_id_type) {}
-profile profiler_summary();
-void profiler_print(const profile& prof, float threshold) {};
-profile profiler_summary() {return profile();}
-region_id_type profiler_region_id(const std::string&) {return 0;}
-std::ostream& operator<<(std::ostream& o, const profile&) {return o;}
+ARB_ARBOR_API void profiler_leave() {}
+ARB_ARBOR_API void profiler_enter(region_id_type) {}
+ARB_ARBOR_API profile profiler_summary();
+ARB_ARBOR_API profile profiler_summary() {return profile();}
+ARB_ARBOR_API region_id_type profiler_region_id(const std::string&) {return 0;}
+ARB_ARBOR_API std::ostream& operator<<(std::ostream& o, const profile&) {return o;}
 
 #endif // ARB_HAVE_PROFILING
 
diff --git a/arbor/s_expr.cpp b/arbor/s_expr.cpp
index fed7878a8173a0d3f28dbbf29faea409906c5348..01e626c3da89663fe90a764fd95d5fb244ffbb63 100644
--- a/arbor/s_expr.cpp
+++ b/arbor/s_expr.cpp
@@ -43,11 +43,11 @@ inline bool is_valid_symbol_char(char c) {
     }
 }
 
-std::ostream& operator<<(std::ostream& o, const src_location& l) {
+ARB_ARBOR_API std::ostream& operator<<(std::ostream& o, const src_location& l) {
     return o << l.line << ":" << l.column;
 }
 
-std::ostream& operator<<(std::ostream& o, const tok& t) {
+ARB_ARBOR_API std::ostream& operator<<(std::ostream& o, const tok& t) {
     switch (t) {
         case tok::nil:    return o << "nil";
         case tok::lparen: return o << "lparen";
@@ -62,7 +62,7 @@ std::ostream& operator<<(std::ostream& o, const tok& t) {
     return o << "<unknown>";
 }
 
-std::ostream& operator<<(std::ostream& o, const token& t) {
+ARB_ARBOR_API std::ostream& operator<<(std::ostream& o, const token& t) {
     if (t.kind==tok::string) {
         return o << util::pprintf("\"{}\"", t.spelling);
     }
@@ -432,11 +432,11 @@ std::ostream& print(std::ostream& o, const s_expr& x, int indent) {
     return o << ")";
 }
 
-std::ostream& operator<<(std::ostream& o, const s_expr& x) {
+ARB_ARBOR_API std::ostream& operator<<(std::ostream& o, const s_expr& x) {
     return print(o, x, 1);
 }
 
-std::size_t length(const s_expr& l) {
+ARB_ARBOR_API std::size_t length(const s_expr& l) {
     // The length of an atom is 1.
     if (l.is_atom() && l) {
         return 1;
@@ -448,7 +448,7 @@ std::size_t length(const s_expr& l) {
     return 1+length(l.tail());
 }
 
-src_location location(const s_expr& l) {
+ARB_ARBOR_API src_location location(const s_expr& l) {
     if (l.is_atom()) return l.atom().loc;
     return location(l.head());
 }
@@ -514,7 +514,7 @@ s_expr parse(lexer& L) {
 
 }
 
-s_expr parse_s_expr(const std::string& line) {
+ARB_ARBOR_API s_expr parse_s_expr(const std::string& line) {
     lexer l(line.c_str());
     s_expr result = impl::parse(l);
     const bool err = result.is_atom()? result.atom().kind==tok::error: false;
diff --git a/arbor/simulation.cpp b/arbor/simulation.cpp
index 300b9947da1111faaeebfbc558a1a504be263f19..f0a6cccaa303304dbd83d567c1522a2e9ca50bdb 100644
--- a/arbor/simulation.cpp
+++ b/arbor/simulation.cpp
@@ -2,6 +2,7 @@
 #include <set>
 #include <vector>
 
+#include <arbor/export.hpp>
 #include <arbor/arbexcept.hpp>
 #include <arbor/context.hpp>
 #include <arbor/domain_decomposition.hpp>
@@ -36,7 +37,7 @@ auto split_sorted_range(Seq&& seq, const Value& v, Less cmp = Less{}) {
 
 // Create a new cell event_lane vector from sorted pending events, previous event_lane events,
 // and events from event generators for the given interval.
-void merge_cell_events(
+ARB_ARBOR_API void merge_cell_events(
     time_type t_from,
     time_type t_to,
     event_span old_events,
diff --git a/arbor/spike_event_io.cpp b/arbor/spike_event_io.cpp
index f035ae860d9a5d759097825df299282f929f6a8d..639f55c8895294b91ae782a3791673e0b34db9ad 100644
--- a/arbor/spike_event_io.cpp
+++ b/arbor/spike_event_io.cpp
@@ -4,7 +4,7 @@
 
 namespace arb {
 
-std::ostream& operator<<(std::ostream& o, const spike_event& ev) {
+ARB_ARBOR_API std::ostream& operator<<(std::ostream& o, const spike_event& ev) {
      return o << "E[tgt " << ev.target << ", t " << ev.time << ", w " << ev.weight << "]";
 }
 
diff --git a/arbor/spike_source_cell_group.hpp b/arbor/spike_source_cell_group.hpp
index a20772258c25f4138d321ecef2981a4c0cb0c1e2..4a54237e6c87b147e6988bbe3f1e7db94b0df34a 100644
--- a/arbor/spike_source_cell_group.hpp
+++ b/arbor/spike_source_cell_group.hpp
@@ -2,6 +2,7 @@
 
 #include <vector>
 
+#include <arbor/export.hpp>
 #include <arbor/common_types.hpp>
 #include <arbor/recipe.hpp>
 #include <arbor/sampling.hpp>
@@ -14,7 +15,7 @@
 
 namespace arb {
 
-class spike_source_cell_group: public cell_group {
+class ARB_ARBOR_API spike_source_cell_group: public cell_group {
 public:
     spike_source_cell_group(const std::vector<cell_gid_type>& gids, const recipe& rec, cell_label_range& cg_sources, cell_label_range& cg_targets);
 
diff --git a/arbor/thread_private_spike_store.hpp b/arbor/thread_private_spike_store.hpp
index 1293de5b26521311d741785190dee0a8e2e66fcb..ff31f2072847a43ab54006984c6b096d0e7a4e7d 100644
--- a/arbor/thread_private_spike_store.hpp
+++ b/arbor/thread_private_spike_store.hpp
@@ -3,6 +3,7 @@
 #include <memory>
 #include <vector>
 
+#include <arbor/export.hpp>
 #include <arbor/common_types.hpp>
 #include <arbor/spike.hpp>
 
@@ -18,7 +19,7 @@ struct local_spike_store_type;
 /// The thread private buffer of the calling thread.
 /// The insert() and gather() methods add a vector of spikes to the buffer,
 /// and collate all of the buffers into a single vector respectively.
-class thread_private_spike_store {
+class ARB_ARBOR_API thread_private_spike_store {
 public :
     thread_private_spike_store();
     ~thread_private_spike_store();
diff --git a/arbor/threading/threading.hpp b/arbor/threading/threading.hpp
index ad2b1d74a6d5e395319656e7e466d4e45ac1ab7f..06b7e1c99b53fa754198b18278684c2f1b54c8c2 100644
--- a/arbor/threading/threading.hpp
+++ b/arbor/threading/threading.hpp
@@ -14,6 +14,8 @@
 #include <unordered_map>
 #include <utility>
 
+#include <arbor/export.hpp>
+
 namespace arb {
 namespace threading {
 
@@ -68,7 +70,7 @@ struct priority_task {
 
 namespace impl {
 
-class notification_queue {
+class ARB_ARBOR_API notification_queue {
     // Number of priority levels in notification queues.
     static constexpr int n_priority = max_async_task_priority+1;
 
@@ -120,7 +122,7 @@ private:
 
 }// namespace impl
 
-class task_system {
+class ARB_ARBOR_API task_system {
 private:
     // Number of notification queues.
     unsigned count_;
diff --git a/arbor/tree.cpp b/arbor/tree.cpp
index a46718699176774fa3ee5af1a921928d8e72208b..a22501e831ea8cdf94d72549c08650db82d2d381 100644
--- a/arbor/tree.cpp
+++ b/arbor/tree.cpp
@@ -372,7 +372,7 @@ void depth_from_root(const tree& t, tree::iarray& depth, tree::int_type segment)
     }
 }
 
-tree::iarray depth_from_root(const tree& t) {
+ARB_ARBOR_API tree::iarray depth_from_root(const tree& t) {
     tree::iarray depth(t.num_segments());
     depth[0] = 0;
     for (auto c: t.children(0)) {
diff --git a/arbor/tree.hpp b/arbor/tree.hpp
index 23f44fbdab084166b5dd3a7ef7158a09ff1cddd3..137598318a595349dc01cd98dc04bb394f06f15a 100644
--- a/arbor/tree.hpp
+++ b/arbor/tree.hpp
@@ -6,6 +6,7 @@
 #include <numeric>
 #include <vector>
 
+#include <arbor/export.hpp>
 #include <arbor/common_types.hpp>
 
 #include "memory/memory.hpp"
@@ -14,7 +15,7 @@
 
 namespace arb {
 
-class tree {
+class ARB_ARBOR_API tree {
 public:
     using int_type   = cell_lid_type;
     using size_type  = cell_local_size_type;
@@ -113,7 +114,7 @@ private:
 
 // Calculates the depth of each branch from the root of a cell segment tree.
 // The root has depth 0, it's children have depth 1, and so on.
-tree::iarray depth_from_root(const tree& t);
+ARB_ARBOR_API tree::iarray depth_from_root(const tree& t);
 
 // Check if c[0] == 0 and c[i] < 0 holds for i != 0
 // Also handle the valid case of c[0]==value_type(-1)
diff --git a/arbor/version.cpp b/arbor/version.cpp
index 0b2233a29c6a17df73b48920cd1f55f673d6831b..88b693891f53ae9cf31b4f03edc2a8dd959c5ba2 100644
--- a/arbor/version.cpp
+++ b/arbor/version.cpp
@@ -1,14 +1,14 @@
 #include <arbor/version.hpp>
 
 namespace arb {
-const char* source_id = ARB_SOURCE_ID;
-const char* arch = ARB_ARCH;
-const char* build_config = ARB_BUILD_CONFIG;
-const char* version = ARB_VERSION;
+ARB_ARBOR_API const char* source_id = ARB_SOURCE_ID;
+ARB_ARBOR_API const char* arch = ARB_ARCH;
+ARB_ARBOR_API const char* build_config = ARB_BUILD_CONFIG;
+ARB_ARBOR_API const char* version = ARB_VERSION;
 #ifdef ARB_VERSION_DEV
-const char* version_dev = ARB_VERSION_DEV;
+ARB_ARBOR_API const char* version_dev = ARB_VERSION_DEV;
 #else
-const char* version_dev = "";
+ARB_ARBOR_API const char* version_dev = "";
 #endif
-const char* full_build_id = ARB_FULL_BUILD_ID;
+ARB_ARBOR_API const char* full_build_id = ARB_FULL_BUILD_ID;
 }
diff --git a/arborenv/CMakeLists.txt b/arborenv/CMakeLists.txt
index 16128499391839a27eff81acecd6e1e93eee9af0..6651f0108c349486512ff2255d00f49cda052813 100644
--- a/arborenv/CMakeLists.txt
+++ b/arborenv/CMakeLists.txt
@@ -16,6 +16,7 @@ add_library(arborenv ${arborenv-sources})
 add_library(arborenv-public-headers INTERFACE)
 target_include_directories(arborenv-public-headers INTERFACE
     $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
+    $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/include>
     $<INSTALL_INTERFACE:include>
 )
 
@@ -26,6 +27,11 @@ target_include_directories(arborenv-public-headers INTERFACE
 target_link_libraries(arborenv PUBLIC arbor arborenv-public-headers)
 target_link_libraries(arborenv PRIVATE arbor-config-defs arborenv-private-deps)
 
+export_visibility(arborenv)
+
+install(FILES ${CMAKE_CURRENT_BINARY_DIR}/include/arborenv/export.hpp
+    DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/arborenv)
+
 install(DIRECTORY include/arborenv
     DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
     FILES_MATCHING PATTERN "*.hpp")
diff --git a/arborenv/affinity.cpp b/arborenv/affinity.cpp
index 61502cc621d89f3f129f450eab53dea358f37070..469a304ec190b974ee47df2ae78cad7be34ce7e5 100644
--- a/arborenv/affinity.cpp
+++ b/arborenv/affinity.cpp
@@ -2,6 +2,8 @@
 #include <system_error>
 #include <vector>
 
+#include <arborenv/concurrency.hpp>
+
 #ifdef __linux__
 
 #ifndef _GNU_SOURCE
@@ -14,7 +16,7 @@ extern "C" {
 
 namespace arbenv {
 
-std::vector<int> get_affinity() {
+ARB_ARBORENV_API std::vector<int> get_affinity() {
     std::vector<int> cores;
     cpu_set_t cpu_set_mask;
 
@@ -39,7 +41,7 @@ std::vector<int> get_affinity() {
 // No support for non-linux systems.
 namespace arbenv {
 
-std::vector<int> get_affinity() {
+ARB_ARBORENV_API std::vector<int> get_affinity() {
     return {};
 }
 
diff --git a/arborenv/concurrency.cpp b/arborenv/concurrency.cpp
index 208acee4590bda4543138242ba957817c5c9b505..35bafe68b612748c0bdc4ce11ec9f738c3c1cd3f 100644
--- a/arborenv/concurrency.cpp
+++ b/arborenv/concurrency.cpp
@@ -14,7 +14,7 @@ namespace arbenv {
 
 // Take a best guess at the number of threads that can be run concurrently.
 // Will return at least 1.
-unsigned long thread_concurrency() {
+ARB_ARBORENV_API unsigned long thread_concurrency() {
     // Attempt to get count first from affinity information if available.
     unsigned long n = get_affinity().size();
 
diff --git a/arborenv/cuda_api.hpp b/arborenv/cuda_api.hpp
index 14b3a363960a59e2ad92d387343d9b4cc675b268..f3395a5d83ab5f87535385791de1340aa47169d6 100644
--- a/arborenv/cuda_api.hpp
+++ b/arborenv/cuda_api.hpp
@@ -4,10 +4,11 @@
 #include <cuda.h>
 #include <cuda_runtime.h>
 #include <cuda_runtime_api.h>
+#include <arborenv/export.hpp>
 
 using DeviceProp = cudaDeviceProp;
 
-struct api_error_type {
+struct ARB_SYMBOL_VISIBLE api_error_type {
     cudaError_t value;
     api_error_type(cudaError_t e): value(e) {}
 
@@ -42,4 +43,4 @@ inline api_error_type get_device_count(ARGS&&... args) {
 template <typename... ARGS>
 inline api_error_type get_device_properties(ARGS&&... args) {
     return cudaGetDeviceProperties(std::forward<ARGS>(args)...);
-}
\ No newline at end of file
+}
diff --git a/arborenv/default_env.cpp b/arborenv/default_env.cpp
index 1f55acfe45ac45eb0abb869def371639214548a7..ddeff5fe82b6b6db388d9cc621037bd69130f644 100644
--- a/arborenv/default_env.cpp
+++ b/arborenv/default_env.cpp
@@ -13,12 +13,12 @@
 
 namespace arbenv {
 
-unsigned long default_concurrency() {
+ARB_ARBORENV_API unsigned long default_concurrency() {
     unsigned long env_thread = get_env_num_threads();
     return env_thread? env_thread: thread_concurrency();
 }
 
-unsigned long get_env_num_threads() {
+ARB_ARBORENV_API unsigned long get_env_num_threads() {
     constexpr const char* env_var = "ARBENV_NUM_THREADS";
     std::optional<long long> env_val = read_env_integer(env_var, throw_on_invalid);
     if (!env_val) return 0;
@@ -31,7 +31,7 @@ unsigned long get_env_num_threads() {
 
 #ifdef ARB_HAVE_GPU
 
-int default_gpu() {
+ARB_ARBORENV_API int default_gpu() {
     constexpr const char* env_var = "ARBENV_GPU_ID";
     int n_device = -1;
     get_device_count(&n_device); // error => leave n_device == -1
@@ -56,7 +56,7 @@ int default_gpu() {
 
 #else
 
-int default_gpu() {
+ARB_ARBORENV_API int default_gpu() {
     return -1;
 }
 
diff --git a/arborenv/hip_api.hpp b/arborenv/hip_api.hpp
index 8a4fe45e267a28f06fa3333dd59d385d58aed2a2..b5417d3a5bfb9727e2dabcd65c7a1b0bfa7b7bd8 100644
--- a/arborenv/hip_api.hpp
+++ b/arborenv/hip_api.hpp
@@ -3,10 +3,11 @@
 
 #include<hip/hip_runtime.h>
 #include<hip/hip_runtime_api.h>
+#include <arborenv/export.hpp>
 
 using DeviceProp = hipDeviceProp_t;
 
-struct api_error_type {
+struct ARB_SYMBOL_VISIBLE api_error_type {
     hipError_t value;
     api_error_type(hipError_t e): value(e) {}
 
diff --git a/arborenv/include/arborenv/arbenvexcept.hpp b/arborenv/include/arborenv/arbenvexcept.hpp
index 7b140ccfec61a2a2095642dd502a7a1e484c0996..49fba2c4dd6a685e84ee569bb8d4764a5c3eb521 100644
--- a/arborenv/include/arborenv/arbenvexcept.hpp
+++ b/arborenv/include/arborenv/arbenvexcept.hpp
@@ -3,13 +3,15 @@
 #include <stdexcept>
 #include <string>
 
+#include <arborenv/export.hpp>
+
 // Arborenv-specific exception hierarchy.
 
 namespace arbenv {
 
 // Common base-class for arborenv run-time errors.
 
-struct arborenv_exception: std::runtime_error {
+struct ARB_SYMBOL_VISIBLE arborenv_exception: std::runtime_error {
     arborenv_exception(const std::string& what_arg):
         std::runtime_error(what_arg)
     {}
@@ -17,7 +19,7 @@ struct arborenv_exception: std::runtime_error {
 
 // Environment variable parsing errors.
 
-struct invalid_env_value: arborenv_exception {
+struct ARB_SYMBOL_VISIBLE invalid_env_value: arborenv_exception {
     invalid_env_value(const std::string& variable, const std::string& value);
     std::string env_variable;
     std::string env_value;
@@ -25,12 +27,12 @@ struct invalid_env_value: arborenv_exception {
 
 // GPU enumeration, selection.
 
-struct no_such_gpu: arborenv_exception {
+struct ARB_SYMBOL_VISIBLE no_such_gpu: arborenv_exception {
     no_such_gpu(int gpu_id);
     int gpu_id;
 };
 
-struct gpu_uuid_error: arborenv_exception {
+struct ARB_SYMBOL_VISIBLE gpu_uuid_error: arborenv_exception {
     gpu_uuid_error(std::string what);
 };
 
diff --git a/arborenv/include/arborenv/concurrency.hpp b/arborenv/include/arborenv/concurrency.hpp
index 25b0f7ac2ad1c1eeea11f32291e9097d62ec9d09..0d7d91f760a706ed5986aa8fae61beb7be7ba5b9 100644
--- a/arborenv/include/arborenv/concurrency.hpp
+++ b/arborenv/include/arborenv/concurrency.hpp
@@ -2,12 +2,14 @@
 
 #include <vector>
 
+#include <arborenv/export.hpp>
+
 namespace arbenv {
 
 // Attempt to determine number of available threads that can be run concurrently.
 // Will return at least 1.
 
-unsigned long thread_concurrency();
+ARB_ARBORENV_API unsigned long thread_concurrency();
 
 // The list of logical processors for which the calling thread has affinity.
 // If calling from the main thread at application start up, before
@@ -19,6 +21,6 @@ unsigned long thread_concurrency();
 // Returns an empty vector if unable to determine the number of
 // available cores.
 
-std::vector<int> get_affinity();
+ARB_ARBORENV_API std::vector<int> get_affinity();
 
 } // namespace arbenv
diff --git a/arborenv/include/arborenv/default_env.hpp b/arborenv/include/arborenv/default_env.hpp
index 843503b678ef1b6a0e87bdd9547f319c428a897a..136df0509d776de2a5a2f252a577d217dd250a00 100644
--- a/arborenv/include/arborenv/default_env.hpp
+++ b/arborenv/include/arborenv/default_env.hpp
@@ -8,6 +8,7 @@
 #include <arborenv/arbenvexcept.hpp>
 #include <arborenv/concurrency.hpp>
 #include <arborenv/gpu_env.hpp>
+#include <arborenv/export.hpp>
 
 namespace arbenv {
 
@@ -15,7 +16,7 @@ namespace arbenv {
 // if set and non-zero, throwing arbev::invalid_env_value if it has an invalid
 // value, or else return the value determined by arbenv::thread_concurrency().
 
-unsigned long default_concurrency();
+ARB_ARBORENV_API unsigned long default_concurrency();
 
 // If Arbor is built without GPU support, return -1.
 //
@@ -29,7 +30,7 @@ unsigned long default_concurrency();
 // Throws arbenv::invalid_env_value if ARBENV_GPU_ID is not an int value, or
 // arbenv::no_such_gpu if it doesn't correspond to a valid GPU id.
 
-int default_gpu();
+ARB_ARBORENV_API int default_gpu();
 
 // Construct default proc_allocation from `default_concurrency()` and
 // `default_gpu()`.
@@ -44,6 +45,6 @@ inline arb::proc_allocation default_allocation() {
 //   non-numeric, non-positive, or out of range value.
 // * Returns zero if ARBENV_NUM_THREADS is unset, or set and empty.
 
-unsigned long get_env_num_threads();
+ARB_ARBORENV_API unsigned long get_env_num_threads();
 
 } // namespace arbenv
diff --git a/arborenv/include/arborenv/gpu_env.hpp b/arborenv/include/arborenv/gpu_env.hpp
index 2aefde7bae60862997405ce1595de5521ad14028..069e44c2713e0b8adde4dc9b44b0ce758f113f39 100644
--- a/arborenv/include/arborenv/gpu_env.hpp
+++ b/arborenv/include/arborenv/gpu_env.hpp
@@ -1,9 +1,11 @@
 #pragma once
 
+#include <arborenv/export.hpp>
+
 namespace arbenv {
 
 template <typename Comm>
-int find_private_gpu(Comm comm);
+ARB_ARBORENV_API int find_private_gpu(Comm comm);
 
 } // namespace arbenv
 
diff --git a/arborenv/private_gpu.cpp b/arborenv/private_gpu.cpp
index 23376454506a215669638bb6866d0e84bade1b54..ee5cd531d294f70e9914075d67c6f1acc6c83425 100644
--- a/arborenv/private_gpu.cpp
+++ b/arborenv/private_gpu.cpp
@@ -14,7 +14,7 @@ namespace arbenv {
 #ifdef ARB_HAVE_GPU
 
 template <>
-int find_private_gpu(MPI_Comm comm) {
+ARB_ARBORENV_API int find_private_gpu(MPI_Comm comm) {
     int nranks;
     int rank;
     MPI_Comm_rank(comm, &rank);
@@ -96,7 +96,7 @@ int find_private_gpu(MPI_Comm comm) {
 
 // return -1 -> "no gpu" when compiled without GPU support.
 template <>
-int find_private_gpu(MPI_Comm comm) {
+ARB_ARBORENV_API int find_private_gpu(MPI_Comm comm) {
     return -1;
 }
 
diff --git a/arborio/CMakeLists.txt b/arborio/CMakeLists.txt
index 4c1eff4abcc8d7ba00ed414cdaf8e8fc9817f9e1..6db6b46fc27f2a8930dd95387a67b2854c9b5482 100644
--- a/arborio/CMakeLists.txt
+++ b/arborio/CMakeLists.txt
@@ -23,6 +23,7 @@ add_library(arborio-private-headers INTERFACE)
 
 target_include_directories(arborio-public-headers INTERFACE
     $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
+    $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/include>
     $<INSTALL_INTERFACE:include>
 )
 target_include_directories(arborio-private-headers INTERFACE
@@ -40,6 +41,11 @@ endif()
 
 target_link_libraries(arborio PRIVATE arbor-config-defs arborio-private-deps)
 
+export_visibility(arborio)
+
+install(FILES ${CMAKE_CURRENT_BINARY_DIR}/include/arborio/export.hpp
+    DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/arborio)
+
 install(DIRECTORY include/arborio
     DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
     FILES_MATCHING PATTERN "*.hpp")
diff --git a/arborio/cableio.cpp b/arborio/cableio.cpp
index 25f6d6e2631d0dc1ad6e0b64b297228d433a287a..88eefd723e2e0d06bad91d62a8ecc025758b597a 100644
--- a/arborio/cableio.cpp
+++ b/arborio/cableio.cpp
@@ -17,7 +17,7 @@ namespace arborio {
 
 using namespace arb;
 
-std::string acc_version() {return "0.1-dev";}
+ARB_ARBORIO_API std::string acc_version() {return "0.1-dev";}
 
 cableio_parse_error::cableio_parse_error(const std::string& msg, const arb::src_location& loc):
     arb::arbor_exception(msg+" at :"+
@@ -153,32 +153,32 @@ s_expr mksexp(const meta_data& meta) {
 }
 
 // Implement public facing s-expr writers
-std::ostream& write_component(std::ostream& o, const decor& x, const meta_data& m) {
+ARB_ARBORIO_API std::ostream& write_component(std::ostream& o, const decor& x, const meta_data& m) {
     if (m.version != acc_version()) {
         throw cableio_version_error(m.version);
     }
     return o << s_expr{"arbor-component"_symbol, slist(mksexp(m), mksexp(x))};
 }
-std::ostream& write_component(std::ostream& o, const label_dict& x, const meta_data& m) {
+ARB_ARBORIO_API std::ostream& write_component(std::ostream& o, const label_dict& x, const meta_data& m) {
     if (m.version != acc_version()) {
         throw cableio_version_error(m.version);
     }
     return o << s_expr{"arbor-component"_symbol, slist(mksexp(m), mksexp(x))};
 }
-std::ostream& write_component(std::ostream& o, const morphology& x, const meta_data& m) {
+ARB_ARBORIO_API std::ostream& write_component(std::ostream& o, const morphology& x, const meta_data& m) {
     if (m.version != acc_version()) {
         throw cableio_version_error(m.version);
     }
     return o << s_expr{"arbor-component"_symbol, slist(mksexp(m), mksexp(x))};
 }
-std::ostream& write_component(std::ostream& o, const cable_cell& x, const meta_data& m) {
+ARB_ARBORIO_API std::ostream& write_component(std::ostream& o, const cable_cell& x, const meta_data& m) {
     if (m.version != acc_version()) {
         throw cableio_version_error(m.version);
     }
     auto cell = s_expr{"cable-cell"_symbol, slist(mksexp(x.morphology()), mksexp(x.labels()), mksexp(x.decorations()))};
     return o << s_expr{"arbor-component"_symbol, slist(mksexp(m), cell)};
 }
-std::ostream& write_component(std::ostream& o, const cable_cell_component& x) {
+ARB_ARBORIO_API std::ostream& write_component(std::ostream& o, const cable_cell_component& x) {
     if (x.meta.version != acc_version()) {
         throw cableio_version_error(x.meta.version);
     }
@@ -665,12 +665,12 @@ inline parse_hopefully<std::any> parse(const arb::s_expr& s) {
     return eval(std::move(s), named_evals, unnamed_evals);
 }
 
-parse_hopefully<std::any> parse_expression(const std::string& s) {
+ARB_ARBORIO_API parse_hopefully<std::any> parse_expression(const std::string& s) {
     return parse(parse_s_expr(s));
 }
 
 // Read s-expr
-parse_hopefully<cable_cell_component> parse_component(const std::string& s) {
+ARB_ARBORIO_API parse_hopefully<cable_cell_component> parse_component(const std::string& s) {
     auto sexp = parse_s_expr(s);
     auto try_parse = parse(sexp);
     if (!try_parse) {
@@ -686,7 +686,7 @@ parse_hopefully<cable_cell_component> parse_component(const std::string& s) {
     return comp;
 };
 
-parse_hopefully<cable_cell_component> parse_component(std::istream& s) {
+ARB_ARBORIO_API parse_hopefully<cable_cell_component> parse_component(std::istream& s) {
     return parse_component(std::string(std::istreambuf_iterator<char>(s), {}));
 }
 } // namespace arborio
diff --git a/arborio/cv_policy_parse.cpp b/arborio/cv_policy_parse.cpp
index 16ad8160861eae2b714f860e5a415052398c07d0..7e5bc2fe4200f9a531743a7fc6d064dd5494f3a6 100644
--- a/arborio/cv_policy_parse.cpp
+++ b/arborio/cv_policy_parse.cpp
@@ -188,7 +188,7 @@ parse_hopefully<std::any> eval(const s_expr& e) {
 }
 }
 
-parse_cv_policy_hopefully parse_cv_policy_expression(const arb::s_expr& s) {
+ARB_ARBORIO_API parse_cv_policy_hopefully parse_cv_policy_expression(const arb::s_expr& s) {
     if (auto e = eval(s)) {
         if (e->type() == typeid(cv_policy)) {
             return {std::move(std::any_cast<cv_policy&>(*e))};
@@ -200,7 +200,7 @@ parse_cv_policy_hopefully parse_cv_policy_expression(const arb::s_expr& s) {
         return util::unexpected(cv_policy_parse_error(std::string() + e.error().what()));
     }
 }
-parse_cv_policy_hopefully parse_cv_policy_expression(const std::string& s) {
+ARB_ARBORIO_API parse_cv_policy_hopefully parse_cv_policy_expression(const std::string& s) {
     return parse_cv_policy_expression(parse_s_expr(s));
 }
 } // namespace arb
diff --git a/arborio/include/arborio/cableio.hpp b/arborio/include/arborio/cableio.hpp
index 7502e7510bf9b114a35ed92c3fd41af235a1adb3..cbdeb146d41a0a3a476465381d6480771e1d42c2 100644
--- a/arborio/include/arborio/cableio.hpp
+++ b/arborio/include/arborio/cableio.hpp
@@ -2,19 +2,20 @@
 
 #include <arbor/cable_cell.hpp>
 #include <arbor/s_expr.hpp>
+#include <arborio/export.hpp>
 
 namespace arborio {
-std::string acc_version();
+ARB_ARBORIO_API std::string acc_version();
 
-struct cableio_parse_error: arb::arbor_exception {
+struct ARB_SYMBOL_VISIBLE cableio_parse_error: arb::arbor_exception {
     explicit cableio_parse_error(const std::string& msg, const arb::src_location& loc);
 };
 
-struct cableio_morphology_error: arb::arbor_exception {
+struct ARB_SYMBOL_VISIBLE cableio_morphology_error: arb::arbor_exception {
     explicit cableio_morphology_error(const unsigned bid);
 };
 
-struct cableio_version_error: arb::arbor_exception {
+struct ARB_SYMBOL_VISIBLE cableio_version_error: arb::arbor_exception {
     explicit cableio_version_error(const std::string& version);
 };
 
@@ -30,13 +31,13 @@ struct cable_cell_component {
     cable_cell_variant component;
 };
 
-std::ostream& write_component(std::ostream&, const cable_cell_component&);
-std::ostream& write_component(std::ostream&, const arb::decor& x, const meta_data& m = {});
-std::ostream& write_component(std::ostream&, const arb::label_dict& x, const meta_data& m = {});
-std::ostream& write_component(std::ostream&, const arb::morphology& x, const meta_data& m = {});
-std::ostream& write_component(std::ostream&, const arb::cable_cell& x, const meta_data& m = {});
+ARB_ARBORIO_API std::ostream& write_component(std::ostream&, const cable_cell_component&);
+ARB_ARBORIO_API std::ostream& write_component(std::ostream&, const arb::decor& x, const meta_data& m = {});
+ARB_ARBORIO_API std::ostream& write_component(std::ostream&, const arb::label_dict& x, const meta_data& m = {});
+ARB_ARBORIO_API std::ostream& write_component(std::ostream&, const arb::morphology& x, const meta_data& m = {});
+ARB_ARBORIO_API std::ostream& write_component(std::ostream&, const arb::cable_cell& x, const meta_data& m = {});
 
-parse_hopefully<cable_cell_component> parse_component(const std::string&);
-parse_hopefully<cable_cell_component> parse_component(std::istream&);
+ARB_ARBORIO_API parse_hopefully<cable_cell_component> parse_component(const std::string&);
+ARB_ARBORIO_API parse_hopefully<cable_cell_component> parse_component(std::istream&);
 
-} // namespace arborio
\ No newline at end of file
+} // namespace arborio
diff --git a/arborio/include/arborio/cv_policy_parse.hpp b/arborio/include/arborio/cv_policy_parse.hpp
index 6069740a9f509097356682ac5cd478642b1368d8..70836105c7326e8ea7bbe1149f24feee3d51b7b7 100644
--- a/arborio/include/arborio/cv_policy_parse.hpp
+++ b/arborio/include/arborio/cv_policy_parse.hpp
@@ -7,18 +7,19 @@
 #include <arbor/arbexcept.hpp>
 #include <arbor/util/expected.hpp>
 #include <arbor/s_expr.hpp>
+#include <arborio/export.hpp>
 
 namespace arborio {
 
-struct cv_policy_parse_error: arb::arbor_exception {
+struct ARB_SYMBOL_VISIBLE cv_policy_parse_error: arb::arbor_exception {
     explicit cv_policy_parse_error(const std::string& msg, const arb::src_location& loc);
     explicit cv_policy_parse_error(const std::string& msg);
 };
 
 using parse_cv_policy_hopefully = arb::util::expected<arb::cv_policy, cv_policy_parse_error>;
 
-parse_cv_policy_hopefully parse_cv_policy_expression(const std::string& s);
-parse_cv_policy_hopefully parse_cv_policy_expression(const arb::s_expr& s);
+ARB_ARBORIO_API parse_cv_policy_hopefully parse_cv_policy_expression(const std::string& s);
+ARB_ARBORIO_API parse_cv_policy_hopefully parse_cv_policy_expression(const arb::s_expr& s);
 
 namespace literals {
 
diff --git a/arborio/include/arborio/label_parse.hpp b/arborio/include/arborio/label_parse.hpp
index b51f9361e67e3c87cc9b8ff46b57fd102f30fa2f..5f96ede80817da7c505f8fb05ebfc7f3c2254eca 100644
--- a/arborio/include/arborio/label_parse.hpp
+++ b/arborio/include/arborio/label_parse.hpp
@@ -9,10 +9,11 @@
 #include <arbor/util/expected.hpp>
 
 #include <arbor/s_expr.hpp>
+#include <arborio/export.hpp>
 
 namespace arborio {
 
-struct label_parse_error: arb::arbor_exception {
+struct ARB_SYMBOL_VISIBLE label_parse_error: arb::arbor_exception {
     explicit label_parse_error(const std::string& msg, const arb::src_location& loc);
     explicit label_parse_error(const std::string& msg): arb::arbor_exception(msg) {}
 };
@@ -20,11 +21,11 @@ struct label_parse_error: arb::arbor_exception {
 template <typename T>
 using parse_label_hopefully = arb::util::expected<T, label_parse_error>;
 
-parse_label_hopefully<std::any> parse_label_expression(const std::string&);
-parse_label_hopefully<std::any> parse_label_expression(const arb::s_expr&);
+ARB_ARBORIO_API parse_label_hopefully<std::any> parse_label_expression(const std::string&);
+ARB_ARBORIO_API parse_label_hopefully<std::any> parse_label_expression(const arb::s_expr&);
 
-parse_label_hopefully<arb::region> parse_region_expression(const std::string& s);
-parse_label_hopefully<arb::locset> parse_locset_expression(const std::string& s);
+ARB_ARBORIO_API parse_label_hopefully<arb::region> parse_region_expression(const std::string& s);
+ARB_ARBORIO_API parse_label_hopefully<arb::locset> parse_locset_expression(const std::string& s);
 
 namespace literals {
 
diff --git a/arborio/include/arborio/neurolucida.hpp b/arborio/include/arborio/neurolucida.hpp
index 4021329d5e5cab8c9bc01dc0a157904b45e9a436..cf82961380c093048e314d67513c9899138649eb 100644
--- a/arborio/include/arborio/neurolucida.hpp
+++ b/arborio/include/arborio/neurolucida.hpp
@@ -7,18 +7,19 @@
 #include <arbor/arbexcept.hpp>
 #include <arbor/morph/label_dict.hpp>
 #include <arbor/morph/morphology.hpp>
+#include <arborio/export.hpp>
 
 namespace arborio {
 
 // Common base-class for arborio run-time errors.
-struct asc_exception: public arb::arbor_exception {
+struct ARB_SYMBOL_VISIBLE asc_exception: public arb::arbor_exception {
     asc_exception(const std::string& what_arg):
         arb::arbor_exception(what_arg)
     {}
 };
 
 // Generic error parsing asc data.
-struct asc_parse_error: asc_exception {
+struct ARB_SYMBOL_VISIBLE asc_parse_error: asc_exception {
     asc_parse_error(const std::string& error_msg, unsigned line, unsigned column);
     std::string message;
     unsigned line;
@@ -26,7 +27,7 @@ struct asc_parse_error: asc_exception {
 };
 
 // An unsupported ASC description feature was encountered.
-struct asc_unsupported: asc_exception {
+struct ARB_SYMBOL_VISIBLE asc_unsupported: asc_exception {
     asc_unsupported(const std::string& error_msg);
     std::string message;
 };
@@ -39,7 +40,10 @@ struct asc_morphology {
     arb::label_dict labels;
 };
 
+// Perform the parsing of the input as a string.
+ARB_ARBORIO_API asc_morphology parse_asc_string(const char* input);
+
 // Load asc morphology from file with name filename.
-asc_morphology load_asc(std::string filename);
+ARB_ARBORIO_API asc_morphology load_asc(std::string filename);
 
 } // namespace arborio
diff --git a/arborio/include/arborio/neuroml.hpp b/arborio/include/arborio/neuroml.hpp
index eead3b6c054659d547060b2d906c06e798cb4640..27c2663b2e95bccc6049f3199c9ec5ce45ed5896 100644
--- a/arborio/include/arborio/neuroml.hpp
+++ b/arborio/include/arborio/neuroml.hpp
@@ -10,23 +10,24 @@
 
 #include <arbor/morph/label_dict.hpp>
 #include <arbor/morph/morphology.hpp>
+#include <arborio/export.hpp>
 
 namespace arborio {
 
 // Common base-class for neuroml run-time errors.
-struct neuroml_exception: std::runtime_error {
+struct ARB_SYMBOL_VISIBLE neuroml_exception: std::runtime_error {
     neuroml_exception(const std::string& what_arg):
         std::runtime_error(what_arg)
     {}
 };
 
 // Can't parse NeuroML if we don't have a document.
-struct nml_no_document: neuroml_exception {
+struct ARB_SYMBOL_VISIBLE nml_no_document: neuroml_exception {
     nml_no_document();
 };
 
 // Generic error parsing NeuroML data.
-struct nml_parse_error: neuroml_exception {
+struct ARB_SYMBOL_VISIBLE nml_parse_error: neuroml_exception {
     nml_parse_error(const std::string& error_msg, unsigned line = 0);
     std::string error_msg;
     unsigned line;
@@ -35,7 +36,7 @@ struct nml_parse_error: neuroml_exception {
 // NeuroML morphology error: improper segment data, e.g. bad id specification,
 // segment parent does not exist, fractionAlong is out of bounds, missing
 // required <proximal> data.
-struct nml_bad_segment: neuroml_exception {
+struct ARB_SYMBOL_VISIBLE nml_bad_segment: neuroml_exception {
     nml_bad_segment(unsigned long long segment_id, unsigned line = 0);
     unsigned long long segment_id;
     unsigned line;
@@ -43,7 +44,7 @@ struct nml_bad_segment: neuroml_exception {
 
 // NeuroML morphology error: improper segmentGroup data, e.g. malformed
 // element data, missing referenced segments or groups, etc.
-struct nml_bad_segment_group: neuroml_exception {
+struct ARB_SYMBOL_VISIBLE nml_bad_segment_group: neuroml_exception {
     nml_bad_segment_group(const std::string& group_id, unsigned line = 0);
     std::string group_id;
     unsigned line;
@@ -51,7 +52,7 @@ struct nml_bad_segment_group: neuroml_exception {
 
 // A segment or segmentGroup ultimately refers to itself via `parent`
 // or `include` elements respectively.
-struct nml_cyclic_dependency: neuroml_exception {
+struct ARB_SYMBOL_VISIBLE nml_cyclic_dependency: neuroml_exception {
     nml_cyclic_dependency(const std::string& id, unsigned line = 0);
     std::string id;
     unsigned line;
@@ -88,7 +89,7 @@ struct nml_morphology_data {
 
 // Represent NeuroML data determined by provided string.
 
-struct neuroml_impl;
+struct ARB_ARBORIO_API neuroml_impl;
 
 // TODO: C++20, replace with enum class and deploy using enum as appropriate.
 namespace neuroml_options {
@@ -98,7 +99,7 @@ namespace neuroml_options {
     };
 }
 
-struct neuroml {
+struct ARB_ARBORIO_API neuroml {
     // Correct interpretation of zero-length segments is currently a bit unclear
     // in NeuroML 2.0, hence these options. For further options, use flags in powers of two
     // so that we can bitwise combine and test them.
diff --git a/arborio/include/arborio/swcio.hpp b/arborio/include/arborio/swcio.hpp
index 2cb76b28693707562dea08013c48d1fb89a4d3ac..26189571ba91a9c7cb21e81ce44fca2a80c339f3 100644
--- a/arborio/include/arborio/swcio.hpp
+++ b/arborio/include/arborio/swcio.hpp
@@ -6,49 +6,50 @@
 
 #include <arbor/arbexcept.hpp>
 #include <arbor/morph/morphology.hpp>
+#include <arborio/export.hpp>
 
 namespace arborio {
 
 // SWC exceptions are thrown by `parse_swc`, and correspond
 // to inconsistent, or in `strict` mode, dubious SWC data.
 
-struct swc_error: public arb::arbor_exception {
+struct ARB_SYMBOL_VISIBLE swc_error: public arb::arbor_exception {
     swc_error(const std::string& msg, int record_id);
     int record_id;
 };
 
 // Parent id in record has no corresponding SWC record,
 // nor is the record the root record with parent id -1.
-struct swc_no_such_parent: swc_error {
+struct ARB_SYMBOL_VISIBLE swc_no_such_parent: swc_error {
     explicit swc_no_such_parent(int record_id);
 };
 
 // Parent id is greater than or equal to record id.
-struct swc_record_precedes_parent: swc_error {
+struct ARB_SYMBOL_VISIBLE swc_record_precedes_parent: swc_error {
     explicit swc_record_precedes_parent(int record_id);
 };
 
 // Multiple records cannot have the same id.
-struct swc_duplicate_record_id: swc_error {
+struct ARB_SYMBOL_VISIBLE swc_duplicate_record_id: swc_error {
     explicit swc_duplicate_record_id(int record_id);
 };
 
 // Smells like a spherical soma.
-struct swc_spherical_soma: swc_error {
+struct ARB_SYMBOL_VISIBLE swc_spherical_soma: swc_error {
     explicit swc_spherical_soma(int record_id);
 };
 
 // Segment cannot have samples with different tags
-struct swc_mismatched_tags: swc_error {
+struct ARB_SYMBOL_VISIBLE swc_mismatched_tags: swc_error {
     explicit swc_mismatched_tags(int record_id);
 };
 
 // Only tags 1, 2, 3, 4 supported
-struct swc_unsupported_tag: swc_error {
+struct ARB_SYMBOL_VISIBLE swc_unsupported_tag: swc_error {
     explicit swc_unsupported_tag(int record_id);
 };
 
-struct swc_record {
+struct ARB_ARBORIO_API swc_record {
     int id = 0;          // sample number
     int tag = 0;         // structure identifier (tag)
     double x = 0;        // sample coordinates
@@ -79,7 +80,7 @@ struct swc_record {
     friend std::istream& operator>>(std::istream&, swc_record&);
 };
 
-struct swc_data {
+struct ARB_ARBORIO_API swc_data {
 private:
     std::string metadata_;
     std::vector<swc_record> records_;
@@ -114,8 +115,8 @@ public:
 //
 // SWC records are returned in id order.
 
-swc_data parse_swc(std::istream&);
-swc_data parse_swc(const std::string&);
+ARB_ARBORIO_API swc_data parse_swc(std::istream&);
+ARB_ARBORIO_API swc_data parse_swc(const std::string&);
 
 // Convert a valid, ordered sequence of SWC records into a morphology.
 //
@@ -126,7 +127,7 @@ swc_data parse_swc(const std::string&);
 // and distal point of the segment, while the proximal point is taken from the
 // parent record.
 
-arb::morphology load_swc_arbor(const swc_data& data);
+ARB_ARBORIO_API arb::morphology load_swc_arbor(const swc_data& data);
 
 // As above, will convert a valid, ordered sequence of SWC records into a morphology
 //
@@ -134,6 +135,6 @@ arb::morphology load_swc_arbor(const swc_data& data);
 //
 // Complies inferred SWC rules from NEURON, explicitly listed in the docs.
 
-arb::morphology load_swc_neuron(const swc_data& data);
+ARB_ARBORIO_API arb::morphology load_swc_neuron(const swc_data& data);
 
 } // namespace arborio
diff --git a/arborio/include/arborio/xml.hpp b/arborio/include/arborio/xml.hpp
index 702827c3c9cdd57b48389ac7400f091ace91130c..bedc54fdf2ee5ca8f084f38fcf578a4eaeed1566 100644
--- a/arborio/include/arborio/xml.hpp
+++ b/arborio/include/arborio/xml.hpp
@@ -3,12 +3,14 @@
 #include <stdexcept>
 #include <string>
 
+#include <arborio/export.hpp>
+
 // XML related interfaces deriving from the underlying XML implementation library.
 
 namespace arborio {
 
 // Generic XML error (as reported by libxml2).
-struct xml_error: std::runtime_error {
+struct ARB_SYMBOL_VISIBLE xml_error: std::runtime_error {
     xml_error(const std::string& xml_error_msg, unsigned line = 0);
     std::string xml_error_msg;
     unsigned line;
@@ -20,7 +22,7 @@ struct xml_error: std::runtime_error {
 // used in a multithreaded context and the client code is
 // not managing libxml2 initialization and cleanup.
 
-struct with_xml {
+struct ARB_ARBORIO_API with_xml {
     with_xml();
     ~with_xml();
 
diff --git a/arborio/label_parse.cpp b/arborio/label_parse.cpp
index 4a2a3ce229ff8cc77b863525fe5855da89813d48..961754cc3cf8b0862ed721e94ba743f6e1bb42b3 100644
--- a/arborio/label_parse.cpp
+++ b/arborio/label_parse.cpp
@@ -213,14 +213,14 @@ parse_label_hopefully<std::any> eval(const s_expr& e) {
 
 } // namespace
 
-parse_label_hopefully<std::any> parse_label_expression(const std::string& e) {
+ARB_ARBORIO_API parse_label_hopefully<std::any> parse_label_expression(const std::string& e) {
     return eval(parse_s_expr(e));
 }
-parse_label_hopefully<std::any> parse_label_expression(const s_expr& s) {
+ARB_ARBORIO_API parse_label_hopefully<std::any> parse_label_expression(const s_expr& s) {
     return eval(s);
 }
 
-parse_label_hopefully<arb::region> parse_region_expression(const std::string& s) {
+ARB_ARBORIO_API parse_label_hopefully<arb::region> parse_region_expression(const std::string& s) {
     if (auto e = eval(parse_s_expr(s))) {
         if (e->type() == typeid(region)) {
             return {std::move(std::any_cast<region&>(*e))};
@@ -237,7 +237,7 @@ parse_label_hopefully<arb::region> parse_region_expression(const std::string& s)
     }
 }
 
-parse_label_hopefully<arb::locset> parse_locset_expression(const std::string& s) {
+ARB_ARBORIO_API parse_label_hopefully<arb::locset> parse_locset_expression(const std::string& s) {
     if (auto e = eval(parse_s_expr(s))) {
         if (e->type() == typeid(locset)) {
             return {std::move(std::any_cast<locset&>(*e))};
diff --git a/arborio/neurolucida.cpp b/arborio/neurolucida.cpp
index fb05007ea832295e52bb3a0c77b736180429c4c7..a2a353e4b682adab75164d3c793399557e0e31e0 100644
--- a/arborio/neurolucida.cpp
+++ b/arborio/neurolucida.cpp
@@ -696,7 +696,7 @@ asc_morphology parse_asc_string(const char* input) {
     return {std::move(morphology), std::move(labels)};
 }
 
-asc_morphology load_asc(std::string filename) {
+ARB_ARBORIO_API asc_morphology load_asc(std::string filename) {
     std::ifstream fid(filename);
 
     if (!fid.good()) {
diff --git a/arborio/neuroml.cpp b/arborio/neuroml.cpp
index 05d594c0132514f4322d79ab61f61f0f14624b87..31dcc80777c40ffaa1ae42dffe5aeb96493f32ea 100644
--- a/arborio/neuroml.cpp
+++ b/arborio/neuroml.cpp
@@ -59,7 +59,7 @@ nml_cyclic_dependency::nml_cyclic_dependency(const std::string& id, unsigned lin
     line(line)
 {}
 
-struct neuroml_impl {
+struct ARB_ARBORIO_API neuroml_impl {
     xml_doc doc;
 
     neuroml_impl() {}
diff --git a/arborio/parse_s_expr.hpp b/arborio/parse_s_expr.hpp
index b240897dd65490bf5b9c852f5dbb9d01eb289bbd..0e6362b794d677a6f575cd32e4257e3029158fed 100644
--- a/arborio/parse_s_expr.hpp
+++ b/arborio/parse_s_expr.hpp
@@ -1,6 +1,7 @@
 #pragma once
 
 #include <arbor/s_expr.hpp>
+#include <arborio/export.hpp>
 #include <arborio/cableio.hpp>
 
 namespace arborio {
@@ -40,6 +41,6 @@ s_expr slist_range(const Range& range) {
     return slist_range(std::begin(range), std::end(range));
 }
 
-parse_hopefully<std::any> parse_expression(const std::string&);
+ARB_ARBORIO_API parse_hopefully<std::any> parse_expression(const std::string&);
 
 } // namespace arborio
diff --git a/arborio/swcio.cpp b/arborio/swcio.cpp
index 1b6bec16f8f648d59abda2f78162e01ebb253d8a..9fc2f2613d5bf8a268a8e6ffd390f0b0ca10d1e1 100644
--- a/arborio/swcio.cpp
+++ b/arborio/swcio.cpp
@@ -51,7 +51,7 @@ swc_unsupported_tag::swc_unsupported_tag(int record_id):
 
 // Record I/O:
 
-std::ostream& operator<<(std::ostream& out, const swc_record& record) {
+ARB_ARBORIO_API std::ostream& operator<<(std::ostream& out, const swc_record& record) {
     std::ios_base::fmtflags flags(out.flags());
 
     out.precision(std::numeric_limits<double>::digits10+2);
@@ -64,7 +64,7 @@ std::ostream& operator<<(std::ostream& out, const swc_record& record) {
     return out;
 }
 
-std::istream& operator>>(std::istream& in, swc_record& record) {
+ARB_ARBORIO_API std::istream& operator>>(std::istream& in, swc_record& record) {
     std::string line;
     if (!getline(in, line, '\n')) return in;
 
@@ -124,7 +124,7 @@ swc_data::swc_data(std::string meta, std::vector<arborio::swc_record> recs) :
 
 // Parse and validate swc data
 
-swc_data parse_swc(std::istream& in) {
+ARB_ARBORIO_API swc_data parse_swc(std::istream& in) {
     // Collect any initial comments (lines beginning with '#').
 
     std::string metadata;
@@ -155,12 +155,12 @@ swc_data parse_swc(std::istream& in) {
     return swc_data(metadata, std::move(records));
 }
 
-swc_data parse_swc(const std::string& text) {
+ARB_ARBORIO_API swc_data parse_swc(const std::string& text) {
     std::istringstream is(text);
     return parse_swc(is);
 }
 
-arb::morphology load_swc_arbor(const swc_data& data) {
+ARB_ARBORIO_API arb::morphology load_swc_arbor(const swc_data& data) {
     const auto& records = data.records();
 
     if (records.empty())  return {};
@@ -205,7 +205,7 @@ arb::morphology load_swc_arbor(const swc_data& data) {
     return arb::morphology(tree);
 }
 
-arb::morphology load_swc_neuron(const swc_data& data) {
+ARB_ARBORIO_API arb::morphology load_swc_neuron(const swc_data& data) {
     constexpr int soma_tag = 1;
 
     const auto n_samples = data.records().size();
diff --git a/cmake/CompilerOptions.cmake b/cmake/CompilerOptions.cmake
index 086726857dc5cd015e4bd825e73c7bba8a47973c..6c1b8eb559c5d7e4680a8dec478210ccbdb54754 100644
--- a/cmake/CompilerOptions.cmake
+++ b/cmake/CompilerOptions.cmake
@@ -136,3 +136,31 @@ function(set_arch_target optvar arch)
     endif()
 
 endfunction()
+
+function(export_visibility target)
+    # mangle target name to correspond to cmake naming
+    string(REPLACE "-" "_" target_name ${target})
+    # extract compact library name
+    string(REPLACE "arbor-" "" target_short_name ${target})
+    # make upper case
+    string(TOUPPER ${target_short_name} target_short_NAME)
+
+    # conditional on build type
+    get_target_property(target_type ${target} TYPE)
+    if (${target_type} STREQUAL STATIC_LIBRARY)
+        # building static library
+        string(CONCAT target_export_def ${target_name} "_EXPORTS_STATIC")
+        target_compile_definitions(${target} PRIVATE ${target_export_def})
+    else()
+        # building shared library
+        string(CONCAT target_export_def ${target_name} "_EXPORTS")
+        # the above compile definition is added by cmake automatically
+    endif()
+
+    # generate config file
+    get_target_property(target_binary_dir ${target} BINARY_DIR)
+    configure_file(
+        ${CMAKE_SOURCE_DIR}/cmake/export.hpp.in
+        ${target_binary_dir}/include/${target_short_name}/export.hpp
+        @ONLY)
+endfunction()
diff --git a/cmake/export.hpp.in b/cmake/export.hpp.in
new file mode 100644
index 0000000000000000000000000000000000000000..655cb4bb2dbdec24f2ef4b7329aab99cd97f0137
--- /dev/null
+++ b/cmake/export.hpp.in
@@ -0,0 +1,41 @@
+#pragma once
+
+//#ifndef ARB_EXPORT_DEBUG
+//#   define ARB_EXPORT_DEBUG
+//#endif
+
+#include <arbor/util/visibility.hpp>
+
+/* library build type (ARB_@target_short_NAME@_STATIC_LIBRARY/ARB_@target_short_NAME@_SHARED_LIBRARY) */
+#define ARB_@target_short_NAME@_@target_type@
+
+#ifndef ARB_@target_short_NAME@_EXPORTS
+#   if defined(@target_name@_EXPORTS)
+        /* we are building @target@ dynamically */
+#       ifdef ARB_EXPORT_DEBUG
+#           pragma message "we are building @target@ dynamically"
+#       endif
+#       define ARB_@target_short_NAME@_API ARB_SYMBOL_EXPORT
+#   elif defined(@target_name@_EXPORTS_STATIC)
+        /* we are building @target@ statically */
+#       ifdef ARB_EXPORT_DEBUG
+#           pragma message "we are building @target@ statically"
+#       endif
+#       define ARB_@target_short_NAME@_API
+#   else
+        /* we are using the library @target@ */
+#       if defined(ARB_@target_short_NAME@_SHARED_LIBRARY)
+            /* we are importing @target@ dynamically */
+#           ifdef ARB_EXPORT_DEBUG
+#              pragma message "we are importing @target@ dynamically"
+#           endif
+#           define ARB_@target_short_NAME@_API ARB_SYMBOL_IMPORT
+#       else
+            /* we are importing @target@ statically */
+#           ifdef ARB_EXPORT_DEBUG
+#               pragma message "we are importing @target@ statically"
+#           endif
+#           define ARB_@target_short_NAME@_API
+#       endif
+#   endif
+#endif
diff --git a/example/drybench/drybench.cpp b/example/drybench/drybench.cpp
index 59b30fa6d17718f24ba6362cf1136ea2c2f0a2f0..ac891cdd551de85540c202b0f41d2d0fdf248c2a 100644
--- a/example/drybench/drybench.cpp
+++ b/example/drybench/drybench.cpp
@@ -143,7 +143,7 @@ int main(int argc, char** argv) {
         auto ctx = arb::make_context(resources);
 
         ctx = arb::make_context(resources, arb::dry_run_info(params.num_ranks, params.num_cells));
-        arb_assert(arb::num_ranks(ctx)==params.num_ranks);
+        arb_assert(arb::num_ranks(ctx)==(unsigned int)params.num_ranks);
 
 #ifdef ARB_PROFILE_ENABLED
         arb::profile::profiler_initialize(ctx);
diff --git a/mechanisms/generate_catalogue b/mechanisms/generate_catalogue
index b0491e5ebfd1dbd3cedb0c828b0008ace2e49397..53eec3af393cb2a93e7d3eb43d28bc245c68416e 100755
--- a/mechanisms/generate_catalogue
+++ b/mechanisms/generate_catalogue
@@ -112,7 +112,7 @@ mechanism_catalogue build_${catalogue}_catalogue() {
     return cat;
 }
 
-const mechanism_catalogue& global_${catalogue}_catalogue() {
+ARB_ARBOR_API const mechanism_catalogue& global_${catalogue}_catalogue() {
     static mechanism_catalogue cat = build_${catalogue}_catalogue();
     return cat;
 }
diff --git a/modcc/CMakeLists.txt b/modcc/CMakeLists.txt
index 32af1a737953610f436b61dfdd717139633dbc02..51857c36103312e8deae7204bc6d98bda391eca8 100644
--- a/modcc/CMakeLists.txt
+++ b/modcc/CMakeLists.txt
@@ -30,17 +30,32 @@ set(libmodcc_sources
 set(modcc_sources modcc.cpp)
 
 add_library(libmodcc STATIC ${libmodcc_sources})
-target_include_directories(libmodcc PUBLIC .)
+target_link_libraries(libmodcc PUBLIC arbor-public-headers)
+
 if (ARB_USE_BUNDLED_FMT)
-    target_include_directories(libmodcc PRIVATE ../ext/fmt/include)
+    target_include_directories(libmodcc
+        PUBLIC
+            $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
+            $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/include>
+        PRIVATE
+            $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../ext/fmt/include>)
+
     target_compile_definitions(libmodcc PRIVATE FMT_HEADER_ONLY)
-else ()
+
+else()
+    target_include_directories(libmodcc
+        PUBLIC
+            $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
+            $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/include>)
     find_package(fmt REQUIRED)
     target_link_libraries(libmodcc PRIVATE fmt::fmt-header-only)
-endif ()
+endif()
 
 set_target_properties(libmodcc PROPERTIES OUTPUT_NAME modcc)
 
+export_visibility(libmodcc)
+
+
 add_executable(modcc ${modcc_sources})
 target_link_libraries(modcc PRIVATE libmodcc ext-tinyopt)
 set_target_properties(modcc libmodcc PROPERTIES EXCLUDE_FROM_ALL ${ARB_WITH_EXTERNAL_MODCC})
diff --git a/modcc/astmanip.cpp b/modcc/astmanip.cpp
index 4d58b09226bc9f75c4930d95f28ceeab73b8e5db..040e340ab8e89698b38f868c205f7c4aad540094 100644
--- a/modcc/astmanip.cpp
+++ b/modcc/astmanip.cpp
@@ -12,7 +12,7 @@ static std::string unique_local_name(scope_ptr scope, std::string const& prefix)
     }
 }
 
-local_assignment make_unique_local_assign(scope_ptr scope, Expression* e, std::string const& prefix) {
+ARB_LIBMODCC_API local_assignment make_unique_local_assign(scope_ptr scope, Expression* e, std::string const& prefix) {
     Location loc = e->location();
     std::string name = unique_local_name(scope, prefix);
 
@@ -28,7 +28,7 @@ local_assignment make_unique_local_assign(scope_ptr scope, Expression* e, std::s
     return { std::move(local), std::move(ass), std::move(id), scope };
 }
 
-local_declaration make_unique_local_decl(scope_ptr scope, Location loc, std::string const& prefix) {
+ARB_LIBMODCC_API local_declaration make_unique_local_decl(scope_ptr scope, Location loc, std::string const& prefix) {
     std::string name = unique_local_name(scope, prefix);
 
     auto local = make_expression<LocalDeclaration>(loc, name);
diff --git a/modcc/astmanip.hpp b/modcc/astmanip.hpp
index e42586ff9b4197e633b7702801a3f133c58ad67f..0e961b8bb7e7e68185e6441e663d421fe38b5359 100644
--- a/modcc/astmanip.hpp
+++ b/modcc/astmanip.hpp
@@ -7,6 +7,7 @@
 #include "expression.hpp"
 #include "location.hpp"
 #include "scope.hpp"
+#include <libmodcc/export.hpp>
 
 // Create new local variable symbol and local declaration expression in current scope.
 // Returns the local declaration expression.
@@ -17,7 +18,7 @@ struct local_declaration {
     scope_ptr scope;
 };
 
-local_declaration make_unique_local_decl(
+ARB_LIBMODCC_API local_declaration make_unique_local_decl(
     scope_ptr scope,
     Location loc,
     std::string const& prefix="ll");
@@ -34,7 +35,7 @@ struct local_assignment {
     scope_ptr scope;
 };
 
-local_assignment make_unique_local_assign(
+ARB_LIBMODCC_API local_assignment make_unique_local_assign(
     scope_ptr scope,
     Expression* e,
     std::string const& prefix="ll");
diff --git a/modcc/blocks.cpp b/modcc/blocks.cpp
index bc9d57ec25e6d5a3512ac7451051c92bb27b6aae..8e979ca596252feb962e1f4cfa9c9c314bb41709 100644
--- a/modcc/blocks.cpp
+++ b/modcc/blocks.cpp
@@ -18,7 +18,7 @@ std::ostream& operator<<(std::ostream& os, const std::vector<T>& v) {
     return os;
 }
 
-std::ostream& operator<<(std::ostream& os, Id const& V) {
+ARB_LIBMODCC_API std::ostream& operator<<(std::ostream& os, Id const& V) {
     if(V.units.size())
         os << "(" << V.token << "," << V.value << "," << V.units << ")";
     else
@@ -27,19 +27,19 @@ std::ostream& operator<<(std::ostream& os, Id const& V) {
     return os;
 }
 
-std::ostream& operator<<(std::ostream& os, UnitsBlock::units_pair const& p) {
+ARB_LIBMODCC_API std::ostream& operator<<(std::ostream& os, UnitsBlock::units_pair const& p) {
     return os << "(" << p.first << ", " << p.second << ")";
 }
 
-std::ostream& operator<<(std::ostream& os, IonDep const& I) {
+ARB_LIBMODCC_API std::ostream& operator<<(std::ostream& os, IonDep const& I) {
     return os << "(" << I.name << ": read " << I.read << " write " << I.write << ")";
 }
 
-std::ostream& operator<<(std::ostream& os, moduleKind const& k) {
+ARB_LIBMODCC_API std::ostream& operator<<(std::ostream& os, moduleKind const& k) {
     return os << (k==moduleKind::density ? "density" : "point process");
 }
 
-std::ostream& operator<<(std::ostream& os, NeuronBlock const& N) {
+ARB_LIBMODCC_API std::ostream& operator<<(std::ostream& os, NeuronBlock const& N) {
     os << blue("NeuronBlock")     << std::endl;
     os << "  kind       : " << N.kind  << std::endl;
     os << "  name       : " << N.name  << std::endl;
@@ -51,27 +51,27 @@ std::ostream& operator<<(std::ostream& os, NeuronBlock const& N) {
     return os;
 }
 
-std::ostream& operator<<(std::ostream& os, StateBlock const& B) {
+ARB_LIBMODCC_API std::ostream& operator<<(std::ostream& os, StateBlock const& B) {
     os << blue("StateBlock")      << std::endl;
     return os << "  variables  : " << B.state_variables << std::endl;
 
 }
 
-std::ostream& operator<<(std::ostream& os, UnitsBlock const& U) {
+ARB_LIBMODCC_API std::ostream& operator<<(std::ostream& os, UnitsBlock const& U) {
     os << blue("UnitsBlock")      << std::endl;
     os << "  aliases    : "  << U.unit_aliases << std::endl;
 
     return os;
 }
 
-std::ostream& operator<<(std::ostream& os, ParameterBlock const& P) {
+ARB_LIBMODCC_API std::ostream& operator<<(std::ostream& os, ParameterBlock const& P) {
     os << blue("ParameterBlock")   << std::endl;
     os << "  parameters : "  << P.parameters << std::endl;
 
     return os;
 }
 
-std::ostream& operator<<(std::ostream& os, AssignedBlock const& A) {
+ARB_LIBMODCC_API std::ostream& operator<<(std::ostream& os, AssignedBlock const& A) {
     os << blue("AssignedBlock")   << std::endl;
     os << "  parameters : "  << A.parameters << std::endl;
 
diff --git a/modcc/blocks.hpp b/modcc/blocks.hpp
index df77a04939160b2a8dbbb4bb7186e376195abdc5..495a47ac0337b02a2b3f38b748bf8ccca7f66758 100644
--- a/modcc/blocks.hpp
+++ b/modcc/blocks.hpp
@@ -8,6 +8,7 @@
 #include "identifier.hpp"
 #include "location.hpp"
 #include "token.hpp"
+#include <libmodcc/export.hpp>
 
 // describes a relationship with an ion channel
 struct IonDep {
@@ -161,20 +162,20 @@ struct AssignedBlock {
 // helpers for pretty printing block information
 ////////////////////////////////////////////////
 
-std::ostream& operator<<(std::ostream& os, Id const& V);
+ARB_LIBMODCC_API std::ostream& operator<<(std::ostream& os, Id const& V);
 
-std::ostream& operator<<(std::ostream& os, UnitsBlock::units_pair const& p);
+ARB_LIBMODCC_API std::ostream& operator<<(std::ostream& os, UnitsBlock::units_pair const& p);
 
-std::ostream& operator<<(std::ostream& os, IonDep const& I);
+ARB_LIBMODCC_API std::ostream& operator<<(std::ostream& os, IonDep const& I);
 
-std::ostream& operator<<(std::ostream& os, moduleKind const& k);
+ARB_LIBMODCC_API std::ostream& operator<<(std::ostream& os, moduleKind const& k);
 
-std::ostream& operator<<(std::ostream& os, NeuronBlock const& N);
+ARB_LIBMODCC_API std::ostream& operator<<(std::ostream& os, NeuronBlock const& N);
 
-std::ostream& operator<<(std::ostream& os, StateBlock const& B);
+ARB_LIBMODCC_API std::ostream& operator<<(std::ostream& os, StateBlock const& B);
 
-std::ostream& operator<<(std::ostream& os, UnitsBlock const& U);
+ARB_LIBMODCC_API std::ostream& operator<<(std::ostream& os, UnitsBlock const& U);
 
-std::ostream& operator<<(std::ostream& os, ParameterBlock const& P);
+ARB_LIBMODCC_API std::ostream& operator<<(std::ostream& os, ParameterBlock const& P);
 
-std::ostream& operator<<(std::ostream& os, AssignedBlock const& A);
+ARB_LIBMODCC_API std::ostream& operator<<(std::ostream& os, AssignedBlock const& A);
diff --git a/modcc/errorvisitor.hpp b/modcc/errorvisitor.hpp
index c9b633857bd0e9c8ca44db8e6f0fd44ae97da07e..c12cfe6617dc35f8fefe4a1b69c61f264783e1ee 100644
--- a/modcc/errorvisitor.hpp
+++ b/modcc/errorvisitor.hpp
@@ -3,8 +3,9 @@
 #include <iostream>
 #include "visitor.hpp"
 #include "expression.hpp"
+#include <libmodcc/export.hpp>
 
-class ErrorVisitor : public Visitor {
+class ARB_LIBMODCC_API ErrorVisitor : public Visitor {
 public:
     ErrorVisitor(std::string const& m)
         : module_name_(m)
diff --git a/modcc/expression.cpp b/modcc/expression.cpp
index 8cb81f7106e592d85570e8964260a305b8ddb36d..8eb9fab2bf0d9de6b33e3e5c168ef8f80bb8ad9c 100644
--- a/modcc/expression.cpp
+++ b/modcc/expression.cpp
@@ -1147,7 +1147,7 @@ void CompartmentExpression::accept(Visitor *v) {
     v->visit(this);
 }
 
-expression_ptr unary_expression( Location loc,
+ARB_LIBMODCC_API expression_ptr unary_expression( Location loc,
                                  tok op,
                                  expression_ptr&& e
                                )
@@ -1178,7 +1178,7 @@ expression_ptr unary_expression( Location loc,
     return nullptr;
 }
 
-expression_ptr binary_expression( tok op,
+ARB_LIBMODCC_API expression_ptr binary_expression( tok op,
                                   expression_ptr&& lhs,
                                   expression_ptr&& rhs
                                 )
@@ -1186,7 +1186,7 @@ expression_ptr binary_expression( tok op,
     return binary_expression(Location(), op, std::move(lhs), std::move(rhs));
 }
 
-expression_ptr binary_expression(Location loc,
+ARB_LIBMODCC_API expression_ptr binary_expression(Location loc,
                                  tok op,
                                  expression_ptr&& lhs,
                                  expression_ptr&& rhs
diff --git a/modcc/expression.hpp b/modcc/expression.hpp
index 280791a7d51cb6f09ba64f533b5d6bade288d0b6..da885630f2080290de61ca1b8c209f14282b0126 100644
--- a/modcc/expression.hpp
+++ b/modcc/expression.hpp
@@ -11,45 +11,46 @@
 #include "identifier.hpp"
 #include "scope.hpp"
 #include "token.hpp"
+#include <libmodcc/export.hpp>
 
 #include "io/pprintf.hpp"
 
 class Visitor;
 
-class Expression;
-class CallExpression;
-class BlockExpression;
-class IfExpression;
-class LocalDeclaration;
-class ArgumentExpression;
-class FunctionExpression;
-class DerivativeExpression;
-class PrototypeExpression;
-class ProcedureExpression;
-class IdentifierExpression;
-class NumberExpression;
-class IntegerExpression;
-class BinaryExpression;
-class UnaryExpression;
-class AssignmentExpression;
-class ConserveExpression;
-class LinearExpression;
-class ReactionExpression;
-class StoichExpression;
-class StoichTermExpression;
-class CompartmentExpression;
-class ConditionalExpression;
-class InitialBlock;
-class SolveExpression;
-class Symbol;
-class ConductanceExpression;
-class PDiffExpression;
-class VariableExpression;
-class NetReceiveExpression;
-class PostEventExpression;
-class APIMethod;
-class IndexedVariable;
-class LocalVariable;
+class ARB_LIBMODCC_API Expression;
+class ARB_LIBMODCC_API CallExpression;
+class ARB_LIBMODCC_API BlockExpression;
+class ARB_LIBMODCC_API IfExpression;
+class ARB_LIBMODCC_API LocalDeclaration;
+class ARB_LIBMODCC_API ArgumentExpression;
+class ARB_LIBMODCC_API FunctionExpression;
+class ARB_LIBMODCC_API DerivativeExpression;
+class ARB_LIBMODCC_API PrototypeExpression;
+class ARB_LIBMODCC_API ProcedureExpression;
+class ARB_LIBMODCC_API IdentifierExpression;
+class ARB_LIBMODCC_API NumberExpression;
+class ARB_LIBMODCC_API IntegerExpression;
+class ARB_LIBMODCC_API BinaryExpression;
+class ARB_LIBMODCC_API UnaryExpression;
+class ARB_LIBMODCC_API AssignmentExpression;
+class ARB_LIBMODCC_API ConserveExpression;
+class ARB_LIBMODCC_API LinearExpression;
+class ARB_LIBMODCC_API ReactionExpression;
+class ARB_LIBMODCC_API StoichExpression;
+class ARB_LIBMODCC_API StoichTermExpression;
+class ARB_LIBMODCC_API CompartmentExpression;
+class ARB_LIBMODCC_API ConditionalExpression;
+class ARB_LIBMODCC_API InitialBlock;
+class ARB_LIBMODCC_API SolveExpression;
+class ARB_LIBMODCC_API Symbol;
+class ARB_LIBMODCC_API ConductanceExpression;
+class ARB_LIBMODCC_API PDiffExpression;
+class ARB_LIBMODCC_API VariableExpression;
+class ARB_LIBMODCC_API NetReceiveExpression;
+class ARB_LIBMODCC_API PostEventExpression;
+class ARB_LIBMODCC_API APIMethod;
+class ARB_LIBMODCC_API IndexedVariable;
+class ARB_LIBMODCC_API LocalVariable;
 
 using expression_ptr = std::unique_ptr<Expression>;
 using symbol_ptr = std::unique_ptr<Symbol>;
@@ -68,10 +69,9 @@ symbol_ptr make_symbol(Args&&... args) {
 }
 
 // helper functions for generating unary and binary expressions
-expression_ptr unary_expression(Location, tok, expression_ptr&&);
-expression_ptr unary_expression(tok, expression_ptr&&);
-expression_ptr binary_expression(Location, tok, expression_ptr&&, expression_ptr&&);
-expression_ptr binary_expression(tok, expression_ptr&&, expression_ptr&&);
+ARB_LIBMODCC_API expression_ptr unary_expression(Location, tok, expression_ptr&&);
+ARB_LIBMODCC_API expression_ptr binary_expression(Location, tok, expression_ptr&&, expression_ptr&&);
+ARB_LIBMODCC_API expression_ptr binary_expression(tok, expression_ptr&&, expression_ptr&&);
 
 /// specifies special properties of a ProcedureExpression
 enum class procedureKind {
@@ -118,7 +118,7 @@ static std::string to_string(solverMethod m) {
     return std::string("<error : undefined solverMethod>");
 }
 
-class Expression {
+class ARB_LIBMODCC_API Expression {
 public:
     explicit Expression(Location location)
     :   location_(location)
@@ -209,7 +209,7 @@ protected:
     scope_ptr scope_;
 };
 
-class Symbol : public Expression {
+class ARB_LIBMODCC_API Symbol : public Expression {
 public :
     Symbol(Location loc, std::string name, symbolKind kind)
     :   Expression(std::move(loc)),
@@ -251,7 +251,7 @@ enum class localVariableKind {
 };
 
 // an identifier
-class IdentifierExpression : public Expression {
+class ARB_LIBMODCC_API IdentifierExpression : public Expression {
 public:
     IdentifierExpression(Location loc, std::string const& spelling)
     :   Expression(loc), spelling_(spelling)
@@ -304,7 +304,7 @@ protected:
 };
 
 // an identifier for a derivative
-class DerivativeExpression : public IdentifierExpression {
+class ARB_LIBMODCC_API DerivativeExpression : public IdentifierExpression {
 public:
     DerivativeExpression(Location loc, std::string const& name)
     :   IdentifierExpression(loc, name)
@@ -326,7 +326,7 @@ public:
 };
 
 // a number
-class NumberExpression : public Expression {
+class ARB_LIBMODCC_API NumberExpression : public Expression {
 public:
     NumberExpression(Location loc, std::string const& value)
         : Expression(loc), value_(std::stold(value))
@@ -356,7 +356,7 @@ private:
 };
 
 // an integral number
-class IntegerExpression : public NumberExpression {
+class ARB_LIBMODCC_API IntegerExpression : public NumberExpression {
 public:
     IntegerExpression(Location loc, std::string const& value)
         : NumberExpression(loc, value), integer_(std::stoll(value))
@@ -388,7 +388,7 @@ private:
 
 
 // declaration of a LOCAL variable
-class LocalDeclaration : public Expression {
+class ARB_LIBMODCC_API LocalDeclaration : public Expression {
 public:
     LocalDeclaration(Location loc)
     :   Expression(loc)
@@ -417,7 +417,7 @@ private:
 };
 
 // declaration of an argument
-class ArgumentExpression : public Expression {
+class ARB_LIBMODCC_API ArgumentExpression : public Expression {
 public:
     ArgumentExpression(Location loc, Token const& tok)
     :   Expression(loc),
@@ -447,7 +447,7 @@ private:
 };
 
 // variable definition
-class VariableExpression : public Symbol {
+class ARB_LIBMODCC_API VariableExpression : public Symbol {
 public:
     VariableExpression(Location loc, std::string name)
     :   Symbol(loc, std::move(name), symbolKind::variable)
@@ -528,7 +528,7 @@ protected:
 // Printers will rewrite reads from or assignments from indexed variables
 // according to its data source and ion channel.
 
-class IndexedVariable : public Symbol {
+class ARB_LIBMODCC_API IndexedVariable : public Symbol {
 public:
     IndexedVariable(Location loc,
                     std::string lookup_name,
@@ -569,7 +569,7 @@ protected:
     sourceKind  data_source_;
 };
 
-class LocalVariable : public Symbol {
+class ARB_LIBMODCC_API LocalVariable : public Symbol {
 public :
     LocalVariable(Location loc,
                   std::string name,
@@ -630,7 +630,7 @@ private :
 
 
 // a SOLVE statement
-class SolveExpression : public Expression {
+class ARB_LIBMODCC_API SolveExpression : public Expression {
 public:
     SolveExpression(
             Location loc,
@@ -685,7 +685,7 @@ private:
 };
 
 // a CONDUCTANCE statement
-class ConductanceExpression : public Expression {
+class ARB_LIBMODCC_API ConductanceExpression : public Expression {
 public:
     ConductanceExpression(
             Location loc,
@@ -729,7 +729,7 @@ private:
 // of Expressions surrounded by {}
 ////////////////////////////////////////////////////////////////////////////////
 
-class BlockExpression : public Expression {
+class ARB_LIBMODCC_API BlockExpression : public Expression {
 protected:
     expr_list_type statements_;
     bool is_nested_ = false;
@@ -777,7 +777,7 @@ public:
     std::string to_string() const override;
 };
 
-class IfExpression : public Expression {
+class ARB_LIBMODCC_API IfExpression : public Expression {
 public:
     IfExpression(Location loc, expression_ptr&& con, expression_ptr&& tb, expression_ptr&& fb)
     :   Expression(loc), condition_(std::move(con)), true_branch_(std::move(tb)), false_branch_(std::move(fb))
@@ -811,7 +811,7 @@ private:
 };
 
 // a proceduce prototype
-class PrototypeExpression : public Expression {
+class ARB_LIBMODCC_API PrototypeExpression : public Expression {
 public:
     PrototypeExpression(
             Location loc,
@@ -877,7 +877,7 @@ private:
     expression_ptr rev_rate_;
 };
 
-class CompartmentExpression : public Expression {
+class ARB_LIBMODCC_API CompartmentExpression : public Expression {
 public:
     CompartmentExpression(Location loc,
                           expression_ptr&& scale_factor,
@@ -904,7 +904,7 @@ private:
     std::vector<expression_ptr> state_vars_;
 };
 
-class StoichTermExpression : public Expression {
+class ARB_LIBMODCC_API StoichTermExpression : public Expression {
 public:
     StoichTermExpression(Location loc,
                          expression_ptr&& coeff,
@@ -938,7 +938,7 @@ private:
     expression_ptr ident_;
 };
 
-class StoichExpression : public Expression {
+class ARB_LIBMODCC_API StoichExpression : public Expression {
 public:
     StoichExpression(Location loc, std::vector<expression_ptr>&& terms)
     : Expression(loc), terms_(std::move(terms))
@@ -964,7 +964,7 @@ private:
 
 // marks a call site in the AST
 // is used to mark both function and procedure calls
-class CallExpression : public Expression {
+class ARB_LIBMODCC_API CallExpression : public Expression {
 public:
     CallExpression(Location loc, std::string spelling, std::vector<expression_ptr>&& args)
     :   Expression(loc), spelling_(std::move(spelling)), args_(std::move(args))
@@ -1010,7 +1010,7 @@ private:
     std::vector<expression_ptr> args_;
 };
 
-class ProcedureExpression : public Symbol {
+class ARB_LIBMODCC_API ProcedureExpression : public Symbol {
 public:
     ProcedureExpression( Location loc,
                          std::string name,
@@ -1063,7 +1063,7 @@ protected:
     procedureKind kind_ = procedureKind::normal;
 };
 
-class APIMethod : public ProcedureExpression {
+class ARB_LIBMODCC_API APIMethod : public ProcedureExpression {
 public:
     APIMethod( Location loc,
                std::string name,
@@ -1082,7 +1082,7 @@ public:
 
 /// stores the INITIAL block in a NET_RECEIVE block, if there is one
 /// should not be used anywhere but NET_RECEIVE
-class InitialBlock : public BlockExpression {
+class ARB_LIBMODCC_API InitialBlock : public BlockExpression {
 public:
     InitialBlock(
         Location loc,
@@ -1102,7 +1102,7 @@ public:
 };
 
 /// handle NetReceiveExpressions as a special case of ProcedureExpression
-class NetReceiveExpression : public ProcedureExpression {
+class ARB_LIBMODCC_API NetReceiveExpression : public ProcedureExpression {
 public:
     NetReceiveExpression( Location loc,
                           std::string name,
@@ -1123,7 +1123,7 @@ protected:
 };
 
 /// handle PostEventExpression as a special case of ProcedureExpression
-class PostEventExpression : public ProcedureExpression {
+class ARB_LIBMODCC_API PostEventExpression : public ProcedureExpression {
 public:
     PostEventExpression( Location loc,
                           std::string name,
@@ -1140,7 +1140,7 @@ public:
     void accept(Visitor *v) override;
 };
 
-class FunctionExpression : public Symbol {
+class ARB_LIBMODCC_API FunctionExpression : public Symbol {
 public:
     FunctionExpression( Location loc,
                         std::string name,
@@ -1192,7 +1192,7 @@ private:
 ////////////////////////////////////////////////////////////
 
 /// Unary expression
-class UnaryExpression : public Expression {
+class ARB_LIBMODCC_API UnaryExpression : public Expression {
 protected:
     expression_ptr expression_;
     tok op_;
@@ -1219,7 +1219,7 @@ public:
 };
 
 /// negation unary expression, i.e. -x
-class NegUnaryExpression : public UnaryExpression {
+class ARB_LIBMODCC_API NegUnaryExpression : public UnaryExpression {
 public:
     NegUnaryExpression(Location loc, expression_ptr e)
     :   UnaryExpression(loc, tok::minus, std::move(e))
@@ -1229,7 +1229,7 @@ public:
 };
 
 /// exponential unary expression, i.e. e^x or exp(x)
-class ExpUnaryExpression : public UnaryExpression {
+class ARB_LIBMODCC_API ExpUnaryExpression : public UnaryExpression {
 public:
     ExpUnaryExpression(Location loc, expression_ptr e)
     :   UnaryExpression(loc, tok::exp, std::move(e))
@@ -1239,7 +1239,7 @@ public:
 };
 
 // logarithm unary expression, i.e. log_10(x)
-class LogUnaryExpression : public UnaryExpression {
+class ARB_LIBMODCC_API LogUnaryExpression : public UnaryExpression {
 public:
     LogUnaryExpression(Location loc, expression_ptr e)
     :   UnaryExpression(loc, tok::log, std::move(e))
@@ -1249,7 +1249,7 @@ public:
 };
 
 // absolute value unary expression, i.e. abs(x)
-class AbsUnaryExpression : public UnaryExpression {
+class ARB_LIBMODCC_API AbsUnaryExpression : public UnaryExpression {
 public:
     AbsUnaryExpression(Location loc, expression_ptr e)
     :   UnaryExpression(loc, tok::abs, std::move(e))
@@ -1258,7 +1258,7 @@ public:
     void accept(Visitor *v) override;
 };
 
-class SafeInvUnaryExpression : public UnaryExpression {
+class ARB_LIBMODCC_API SafeInvUnaryExpression : public UnaryExpression {
 public:
     SafeInvUnaryExpression(Location loc, expression_ptr e)
     :   UnaryExpression(loc, tok::safeinv, std::move(e))
@@ -1269,7 +1269,7 @@ public:
 
 // exprel reciprocal unary expression,
 // i.e. x/(exp(x)-1)=x/expm1(x) with exprelr(0)=1
-class ExprelrUnaryExpression : public UnaryExpression {
+class ARB_LIBMODCC_API ExprelrUnaryExpression : public UnaryExpression {
 public:
     ExprelrUnaryExpression(Location loc, expression_ptr e)
     :   UnaryExpression(loc, tok::exprelr, std::move(e))
@@ -1279,7 +1279,7 @@ public:
 };
 
 // cosine unary expression, i.e. cos(x)
-class CosUnaryExpression : public UnaryExpression {
+class ARB_LIBMODCC_API CosUnaryExpression : public UnaryExpression {
 public:
     CosUnaryExpression(Location loc, expression_ptr e)
     :   UnaryExpression(loc, tok::cos, std::move(e))
@@ -1289,7 +1289,7 @@ public:
 };
 
 // sin unary expression, i.e. sin(x)
-class SinUnaryExpression : public UnaryExpression {
+class ARB_LIBMODCC_API SinUnaryExpression : public UnaryExpression {
 public:
     SinUnaryExpression(Location loc, expression_ptr e)
     :   UnaryExpression(loc, tok::sin, std::move(e))
@@ -1306,7 +1306,7 @@ public:
 /// binary expression base class
 /// never used directly in the AST, instead the specializations that derive from
 /// it are inserted into the AST.
-class BinaryExpression : public Expression {
+class ARB_LIBMODCC_API BinaryExpression : public Expression {
 protected:
     expression_ptr lhs_;
     expression_ptr rhs_;
@@ -1334,7 +1334,7 @@ public:
     void accept(Visitor *v) override;
 };
 
-class AssignmentExpression : public BinaryExpression {
+class ARB_LIBMODCC_API AssignmentExpression : public BinaryExpression {
 public:
     AssignmentExpression(Location loc, expression_ptr&& lhs, expression_ptr&& rhs)
     :   BinaryExpression(loc, tok::eq, std::move(lhs), std::move(rhs))
@@ -1347,7 +1347,7 @@ public:
     void accept(Visitor *v) override;
 };
 
-class ConserveExpression : public BinaryExpression {
+class ARB_LIBMODCC_API ConserveExpression : public BinaryExpression {
 public:
     ConserveExpression(Location loc, expression_ptr&& lhs, expression_ptr&& rhs)
     :   BinaryExpression(loc, tok::eq, std::move(lhs), std::move(rhs))
@@ -1361,7 +1361,7 @@ public:
     void accept(Visitor *v) override;
 };
 
-class LinearExpression : public BinaryExpression {
+class ARB_LIBMODCC_API LinearExpression : public BinaryExpression {
 public:
     LinearExpression(Location loc, expression_ptr&& lhs, expression_ptr&& rhs)
             :   BinaryExpression(loc, tok::eq, std::move(lhs), std::move(rhs))
@@ -1375,7 +1375,7 @@ public:
     void accept(Visitor *v) override;
 };
 
-class AddBinaryExpression : public BinaryExpression {
+class ARB_LIBMODCC_API AddBinaryExpression : public BinaryExpression {
 public:
     AddBinaryExpression(Location loc, expression_ptr&& lhs, expression_ptr&& rhs)
     :   BinaryExpression(loc, tok::plus, std::move(lhs), std::move(rhs))
@@ -1384,7 +1384,7 @@ public:
     void accept(Visitor *v) override;
 };
 
-class SubBinaryExpression : public BinaryExpression {
+class ARB_LIBMODCC_API SubBinaryExpression : public BinaryExpression {
 public:
     SubBinaryExpression(Location loc, expression_ptr&& lhs, expression_ptr&& rhs)
     :   BinaryExpression(loc, tok::minus, std::move(lhs), std::move(rhs))
@@ -1393,7 +1393,7 @@ public:
     void accept(Visitor *v) override;
 };
 
-class MulBinaryExpression : public BinaryExpression {
+class ARB_LIBMODCC_API MulBinaryExpression : public BinaryExpression {
 public:
     MulBinaryExpression(Location loc, expression_ptr&& lhs, expression_ptr&& rhs)
     :   BinaryExpression(loc, tok::times, std::move(lhs), std::move(rhs))
@@ -1402,7 +1402,7 @@ public:
     void accept(Visitor *v) override;
 };
 
-class DivBinaryExpression : public BinaryExpression {
+class ARB_LIBMODCC_API DivBinaryExpression : public BinaryExpression {
 public:
     DivBinaryExpression(Location loc, expression_ptr&& lhs, expression_ptr&& rhs)
     :   BinaryExpression(loc, tok::divide, std::move(lhs), std::move(rhs))
@@ -1411,7 +1411,7 @@ public:
     void accept(Visitor *v) override;
 };
 
-class MinBinaryExpression : public BinaryExpression {
+class ARB_LIBMODCC_API MinBinaryExpression : public BinaryExpression {
 public:
     MinBinaryExpression(Location loc, expression_ptr&& lhs, expression_ptr&& rhs)
     :   BinaryExpression(loc, tok::min, std::move(lhs), std::move(rhs))
@@ -1423,7 +1423,7 @@ public:
     void accept(Visitor *v) override;
 };
 
-class MaxBinaryExpression : public BinaryExpression {
+class ARB_LIBMODCC_API MaxBinaryExpression : public BinaryExpression {
 public:
     MaxBinaryExpression(Location loc, expression_ptr&& lhs, expression_ptr&& rhs)
     :   BinaryExpression(loc, tok::max, std::move(lhs), std::move(rhs))
@@ -1435,7 +1435,7 @@ public:
     void accept(Visitor *v) override;
 };
 
-class PowBinaryExpression : public BinaryExpression {
+class ARB_LIBMODCC_API PowBinaryExpression : public BinaryExpression {
 public:
     PowBinaryExpression(Location loc, expression_ptr&& lhs, expression_ptr&& rhs)
     :   BinaryExpression(loc, tok::pow, std::move(lhs), std::move(rhs))
@@ -1447,7 +1447,7 @@ public:
     void accept(Visitor *v) override;
 };
 
-class ConditionalExpression : public BinaryExpression {
+class ARB_LIBMODCC_API ConditionalExpression : public BinaryExpression {
 public:
     ConditionalExpression(Location loc, tok op, expression_ptr&& lhs, expression_ptr&& rhs)
     :   BinaryExpression(loc, op, std::move(lhs), std::move(rhs))
@@ -1458,7 +1458,7 @@ public:
     void accept(Visitor *v) override;
 };
 
-class PDiffExpression : public Expression {
+class ARB_LIBMODCC_API PDiffExpression : public Expression {
 public:
     PDiffExpression(Location loc, expression_ptr&& var, expression_ptr&& arg)
     :  Expression(loc), var_(std::move(var)), arg_(std::move(arg))
diff --git a/modcc/functionexpander.cpp b/modcc/functionexpander.cpp
index d53e83965897e4fc21ff2040e9f2c7c76b7210b4..e4dde3f0ca9f077141e9e7e547dce70d57a1e607 100644
--- a/modcc/functionexpander.cpp
+++ b/modcc/functionexpander.cpp
@@ -5,7 +5,7 @@
 #include "error.hpp"
 #include "functionexpander.hpp"
 
-expression_ptr insert_unique_local_assignment(expr_list_type& stmts, Expression* e) {
+ARB_LIBMODCC_API expression_ptr insert_unique_local_assignment(expr_list_type& stmts, Expression* e) {
     auto zero = make_expression<NumberExpression>(e->location(), 0.);
     auto exprs = make_unique_local_assign(e->scope(), zero);
 
@@ -34,7 +34,7 @@ expression_ptr insert_unique_local_assignment(expr_list_type& stmts, Expression*
 //       ll0_ = foo(ll1_, y, 1)
 //       a = 2 + ll0_
 /////////////////////////////////////////////////////////////////////
-expression_ptr lower_functions(BlockExpression* block) {
+ARB_LIBMODCC_API expression_ptr lower_functions(BlockExpression* block) {
     auto v = std::make_unique<FunctionCallLowerer>();
     block->accept(v.get());
     return v->as_block(false);
diff --git a/modcc/functionexpander.hpp b/modcc/functionexpander.hpp
index cfb04b7f4f47866af0072aac6a58a0ea446c8eda..7c5671c37df761882628d02a2a8337dbcb24fb97 100644
--- a/modcc/functionexpander.hpp
+++ b/modcc/functionexpander.hpp
@@ -5,16 +5,17 @@
 #include "expression.hpp"
 #include "scope.hpp"
 #include "visitor.hpp"
+#include <libmodcc/export.hpp>
 
 // Make a local declaration and assignment for the given expression,
 // and insert at the front and back respectively of the statement list.
 // Return the new unique local identifier.
-expression_ptr insert_unique_local_assignment(expr_list_type& stmts, Expression* e);
+ARB_LIBMODCC_API expression_ptr insert_unique_local_assignment(expr_list_type& stmts, Expression* e);
 
 // prototype for lowering function calls and arguments
-expression_ptr lower_functions(BlockExpression* block);
+ARB_LIBMODCC_API expression_ptr lower_functions(BlockExpression* block);
 
-class FunctionCallLowerer : public BlockRewriterBase {
+class ARB_LIBMODCC_API FunctionCallLowerer : public BlockRewriterBase {
 public:
     using BlockRewriterBase::visit;
 
diff --git a/modcc/functioninliner.cpp b/modcc/functioninliner.cpp
index ddfcbf6283258826dbff91248ad7a2e21148c9c0..6d727e7f29d5cc6f47508cad5a10b82d49fa6a6f 100644
--- a/modcc/functioninliner.cpp
+++ b/modcc/functioninliner.cpp
@@ -17,7 +17,7 @@
 // argument renaming. This means that if a local variable shadows a function
 // argument, the local variable takes precedence.
 
-expression_ptr inline_function_calls(std::string calling_func, BlockExpression* block) {
+ARB_LIBMODCC_API expression_ptr inline_function_calls(std::string calling_func, BlockExpression* block) {
     auto inline_block = block->clone();
 
     // The function inliner will inline one function at a time
diff --git a/modcc/functioninliner.hpp b/modcc/functioninliner.hpp
index ba4bada981e5ad43def73fe3d699391ec271b9db..2fac9a72cb9844adb4a8b91d53c2324610230262 100644
--- a/modcc/functioninliner.hpp
+++ b/modcc/functioninliner.hpp
@@ -4,10 +4,11 @@
 
 #include "scope.hpp"
 #include "visitor.hpp"
+#include <libmodcc/export.hpp>
 
-expression_ptr inline_function_calls(std::string calling_func, BlockExpression* block);
+ARB_LIBMODCC_API expression_ptr inline_function_calls(std::string calling_func, BlockExpression* block);
 
-class FunctionInliner : public BlockRewriterBase {
+class ARB_LIBMODCC_API FunctionInliner : public BlockRewriterBase {
 public:
     using BlockRewriterBase::visit;
     FunctionInliner(std::string calling_func) : BlockRewriterBase(), calling_func_(calling_func) {};
@@ -61,4 +62,4 @@ protected:
         inlining_executed_ = false;
         BlockRewriterBase::reset();
     }
-};
\ No newline at end of file
+};
diff --git a/modcc/io/prefixbuf.cpp b/modcc/io/prefixbuf.cpp
index 916e795070a221cc1a61379d9a05fae3631ea1d5..6b85f907dfcbae882c7af7728fa616eabf5a31ac 100644
--- a/modcc/io/prefixbuf.cpp
+++ b/modcc/io/prefixbuf.cpp
@@ -56,7 +56,7 @@ prefixbuf::int_type prefixbuf::overflow(int_type ch) {
 
 // setprefix implementation:
 
-std::ostream& operator<<(std::ostream& os, const setprefix& sp) {
+ARB_LIBMODCC_API std::ostream& operator<<(std::ostream& os, const setprefix& sp) {
     if (auto pbuf = dynamic_cast<prefixbuf*>(os.rdbuf())) {
         pbuf->prefix = sp.prefix_;
     }
@@ -111,7 +111,7 @@ static void indent_stack_callback(std::ios_base::event ev, std::ios_base& ios, i
     }
 }
 
-std::ostream& operator<<(std::ostream& os, indent_manip in) {
+ARB_LIBMODCC_API std::ostream& operator<<(std::ostream& os, indent_manip in) {
     int xindex = indent_manip::xindex();
     void*& pword = os.pword(xindex);
     long& iword = os.iword(xindex);
diff --git a/modcc/io/prefixbuf.hpp b/modcc/io/prefixbuf.hpp
index 107637073cc848e34b2bc72848c8a6f7ad489000..34e044ce611c100a9bf4539be294143a42b6e833 100644
--- a/modcc/io/prefixbuf.hpp
+++ b/modcc/io/prefixbuf.hpp
@@ -8,6 +8,8 @@
 #include <sstream>
 #include <string>
 
+#include <libmodcc/export.hpp>
+
 namespace io {
 
 // `prefixbuf` acts an output-only filter for another streambuf, inserting
@@ -28,7 +30,7 @@ namespace io {
 // A flag determines if the prefixbuf should or should not emit the prefix
 // for empty lines.
 
-class prefixbuf: public std::streambuf {
+class ARB_LIBMODCC_API prefixbuf: public std::streambuf {
 public:
     explicit prefixbuf(std::streambuf* inner, bool prefix_empty_lines=false):
         inner_(inner), prefix_empty_lines_(prefix_empty_lines) {}
@@ -75,7 +77,7 @@ protected:
 // The manipulator `indent(0)` can be used to reset the prefix of the underlying
 // stream to match the current indentation level.
 
-class setprefix {
+class ARB_LIBMODCC_API setprefix {
 public:
     explicit setprefix(std::string prefix): prefix_(std::move(prefix)) {}
 
@@ -85,7 +87,7 @@ private:
     std::string prefix_;
 };
 
-struct indent_manip {
+struct ARB_LIBMODCC_API indent_manip {
     enum action_enum {push, pop, settab};
 
     explicit constexpr indent_manip(action_enum action, unsigned value=0):
@@ -119,7 +121,7 @@ inline indent_manip settab(unsigned w) {
 // Acts very much like a `std::ostringstream`, but with prefix
 // and indent functionality.
 
-class pfxstringstream: public std::ostream {
+class ARB_LIBMODCC_API pfxstringstream: public std::ostream {
 public:
     pfxstringstream():
         std::ostream(nullptr),
diff --git a/modcc/kineticrewriter.cpp b/modcc/kineticrewriter.cpp
index bb6ef0b77f3a6857d95521f4b91bed6bdfb21538..b3aef7ab9f6131da52d5f21cb1720066b080f2c8 100644
--- a/modcc/kineticrewriter.cpp
+++ b/modcc/kineticrewriter.cpp
@@ -30,7 +30,7 @@ private:
     std::map<std::string, expression_ptr> dterms;
 };
 
-expression_ptr kinetic_rewrite(BlockExpression* block) {
+ARB_LIBMODCC_API expression_ptr kinetic_rewrite(BlockExpression* block) {
     KineticRewriter visitor;
     block->accept(&visitor);
     return visitor.as_block(false);
diff --git a/modcc/kineticrewriter.hpp b/modcc/kineticrewriter.hpp
index 3dfb2648a1006bc6432e9bd7d082e988b49a0795..3d640eff951dba0ccfcf07e9b98f4d6dd40ab4f4 100644
--- a/modcc/kineticrewriter.hpp
+++ b/modcc/kineticrewriter.hpp
@@ -1,6 +1,7 @@
 #pragma once
 
 #include "expression.hpp"
+#include <libmodcc/export.hpp>
 
 // Translate a supplied KINETIC block to equivalent DERIVATIVE block.
-expression_ptr kinetic_rewrite(BlockExpression*);
+ARB_LIBMODCC_API expression_ptr kinetic_rewrite(BlockExpression*);
diff --git a/modcc/lexer.hpp b/modcc/lexer.hpp
index f897db5856e0df368e1a840aadb3367f35544d39..82690dfa518835e0266f01d0bac31b3ba20828fe 100644
--- a/modcc/lexer.hpp
+++ b/modcc/lexer.hpp
@@ -11,6 +11,7 @@
 #include "location.hpp"
 #include "error.hpp"
 #include "token.hpp"
+#include <libmodcc/export.hpp>
 
 // status of the lexer
 enum class lexerStatus {
@@ -29,7 +30,7 @@ bool is_keyword(Token const& t);
 
 // class that implements the lexer
 // takes a range of characters as input parameters
-class Lexer {
+class ARB_LIBMODCC_API Lexer {
 public:
     Lexer(const char* begin, const char* end)
     :   begin_(begin),
diff --git a/modcc/linearrewriter.cpp b/modcc/linearrewriter.cpp
index e3513694fc5cffda7847e3aa3bdb0a813542f4ff..ea055c5e21ee91e63764b858461152744a3b55b6 100644
--- a/modcc/linearrewriter.cpp
+++ b/modcc/linearrewriter.cpp
@@ -25,7 +25,7 @@ private:
     std::vector<std::string> state_vars;
 };
 
-expression_ptr linear_rewrite(BlockExpression* block, std::vector<std::string> state_vars) {
+ARB_LIBMODCC_API expression_ptr linear_rewrite(BlockExpression* block, std::vector<std::string> state_vars) {
     LinearRewriter visitor(state_vars);
     block->accept(&visitor);
     return visitor.as_block(false);
diff --git a/modcc/linearrewriter.hpp b/modcc/linearrewriter.hpp
index 32b3386334b87c4799dd86034bb0f747772cf892..a517981c0d38e2e4e72510bb8d76f98d41bc31b1 100644
--- a/modcc/linearrewriter.hpp
+++ b/modcc/linearrewriter.hpp
@@ -1,6 +1,7 @@
 #pragma once
 
 #include "expression.hpp"
+#include <libmodcc/export.hpp>
 
 // Translate a supplied LINEAR block.
-expression_ptr linear_rewrite(BlockExpression*, std::vector<std::string>);
+ARB_LIBMODCC_API expression_ptr linear_rewrite(BlockExpression*, std::vector<std::string>);
diff --git a/modcc/module.hpp b/modcc/module.hpp
index 1854bd55c86e82c98d99df5844638665c9110f39..8411328fb9458a5d3be03a38a376e56ca33aab61 100644
--- a/modcc/module.hpp
+++ b/modcc/module.hpp
@@ -8,9 +8,10 @@
 #include "blocks.hpp"
 #include "error.hpp"
 #include "expression.hpp"
+#include <libmodcc/export.hpp>
 
 // wrapper around a .mod file
-class Module: public error_stack {
+class ARB_LIBMODCC_API Module: public error_stack {
 public:
     using symbol_map = scope_type::symbol_map;
     using symbol_ptr = scope_type::symbol_ptr;
diff --git a/modcc/parser.hpp b/modcc/parser.hpp
index 0bdf0ea84b5dfaa779f768575dd2fd5c2572f6a9..84e56316c0acb4dc001c6bf42756912e38d60092 100644
--- a/modcc/parser.hpp
+++ b/modcc/parser.hpp
@@ -7,8 +7,9 @@
 #include "expression.hpp"
 #include "lexer.hpp"
 #include "module.hpp"
+#include <libmodcc/export.hpp>
 
-class Parser: public Lexer {
+class ARB_LIBMODCC_API Parser: public Lexer {
 public:
     explicit Parser(Module& m, bool advance = true);
     Parser(std::string const&);
diff --git a/modcc/printer/cexpr_emit.cpp b/modcc/printer/cexpr_emit.cpp
index e9c23e4d229221d853ac116ad947de6f8935cb2a..6e93c1d0c15b194ea7cac5436f4282dfa5f95149 100644
--- a/modcc/printer/cexpr_emit.cpp
+++ b/modcc/printer/cexpr_emit.cpp
@@ -11,7 +11,7 @@
 #include "astmanip.hpp"
 #include "io/prefixbuf.hpp"
 
-std::ostream& operator<<(std::ostream& out, as_c_double wrap) {
+ARB_LIBMODCC_API std::ostream& operator<<(std::ostream& out, as_c_double wrap) {
     bool neg = std::signbit(wrap.value);
 
     switch (std::fpclassify(wrap.value)) {
diff --git a/modcc/printer/cexpr_emit.hpp b/modcc/printer/cexpr_emit.hpp
index 4b278ec85be11e6ad61369b6abbef9aaaf5c708d..86b5f957e80812e088a8c04bec2109ef781e76a6 100644
--- a/modcc/printer/cexpr_emit.hpp
+++ b/modcc/printer/cexpr_emit.hpp
@@ -6,11 +6,12 @@
 #include "expression.hpp"
 #include "visitor.hpp"
 #include "marks.hpp"
+#include <libmodcc/export.hpp>
 
 // Common functionality for generating source from binary expressions
 // and conditional structures with C syntax.
 
-class CExprEmitter: public Visitor {
+class ARB_LIBMODCC_API CExprEmitter: public Visitor {
 public:
     CExprEmitter(std::ostream& out, Visitor* fallback):
         out_(out), fallback_(fallback)
@@ -37,7 +38,7 @@ inline void cexpr_emit(Expression* e, std::ostream& out, Visitor* fallback) {
     e->accept(&emitter);
 }
 
-class SimdExprEmitter: public CExprEmitter {
+class ARB_LIBMODCC_API SimdExprEmitter: public CExprEmitter {
     using CExprEmitter::visit;
 public:
     SimdExprEmitter(
@@ -95,4 +96,4 @@ struct as_c_double {
     as_c_double(double value): value(value) {}
 };
 
-std::ostream& operator<<(std::ostream&, as_c_double);
+ARB_LIBMODCC_API std::ostream& operator<<(std::ostream&, as_c_double);
diff --git a/modcc/printer/cprinter.cpp b/modcc/printer/cprinter.cpp
index 872f3d4344ffe1affbc2e56ac1e2d3cbcd34e10f..ee417bea075edcde5688ced694534a6471a36d0b 100644
--- a/modcc/printer/cprinter.cpp
+++ b/modcc/printer/cprinter.cpp
@@ -117,7 +117,7 @@ struct simdprint {
     }
 };
 
-std::string emit_cpp_source(const Module& module_, const printer_options& opt) {
+ARB_LIBMODCC_API std::string emit_cpp_source(const Module& module_, const printer_options& opt) {
     auto name           = module_.module_name();
     auto namespace_name = "kernel_" + name;
     auto ppack_name     = "arb_mechanism_ppack";
diff --git a/modcc/printer/cprinter.hpp b/modcc/printer/cprinter.hpp
index b07b7f1b550abeda1b0553010c70ea7cbaa273c3..57f94d5957bd3f1d334fc83da6efe6b90c9574b9 100644
--- a/modcc/printer/cprinter.hpp
+++ b/modcc/printer/cprinter.hpp
@@ -5,15 +5,16 @@
 
 #include "module.hpp"
 #include "visitor.hpp"
+#include <libmodcc/export.hpp>
 
 #include "printer/cexpr_emit.hpp"
 #include "printer/printeropt.hpp"
 
-std::string emit_cpp_source(const Module& m, const printer_options& opt);
+ARB_LIBMODCC_API std::string emit_cpp_source(const Module& m, const printer_options& opt);
 
 // CPrinter and SimdPrinter visitors exposed in header for testing purposes only.
 
-class CPrinter: public Visitor {
+class ARB_LIBMODCC_API CPrinter: public Visitor {
 public:
     CPrinter(std::ostream& out): out_(out) {}
 
@@ -44,7 +45,7 @@ enum class simd_expr_constraint{
     other
 };
 
-class SimdPrinter: public Visitor {
+class ARB_LIBMODCC_API SimdPrinter: public Visitor {
 public:
     SimdPrinter(std::ostream& out): out_(out) {}
 
diff --git a/modcc/printer/gpuprinter.cpp b/modcc/printer/gpuprinter.cpp
index 21a24d9c3011a6deb1168d04afd4bbda75745c9a..d0a69873c1cec738436c52dd10a809064a6041a5 100644
--- a/modcc/printer/gpuprinter.cpp
+++ b/modcc/printer/gpuprinter.cpp
@@ -42,7 +42,7 @@ static std::string ion_field(const IonDep& ion) { return fmt::format("ion_{}",
 static std::string ion_index(const IonDep& ion) { return fmt::format("ion_{}_index", ion.name); }
 
 
-std::string emit_gpu_cpp_source(const Module& module_, const printer_options& opt) {
+ARB_LIBMODCC_API std::string emit_gpu_cpp_source(const Module& module_, const printer_options& opt) {
     std::string name       = module_.module_name();
     std::string class_name = make_class_name(name);
     std::string ppack_name = make_ppack_name(name);
@@ -92,7 +92,7 @@ std::string emit_gpu_cpp_source(const Module& module_, const printer_options& op
     return out.str();
 }
 
-std::string emit_gpu_cu_source(const Module& module_, const printer_options& opt) {
+ARB_LIBMODCC_API std::string emit_gpu_cu_source(const Module& module_, const printer_options& opt) {
     std::string name = module_.module_name();
     std::string class_name = make_class_name(name);
 
diff --git a/modcc/printer/gpuprinter.hpp b/modcc/printer/gpuprinter.hpp
index 9d3a0b7ddc799e4a23762711c01d5b433c2da5da..58f4ecdadb02b053cf5a026b44b622ce45e18ea8 100644
--- a/modcc/printer/gpuprinter.hpp
+++ b/modcc/printer/gpuprinter.hpp
@@ -5,11 +5,12 @@
 #include "cprinter.hpp"
 #include "module.hpp"
 #include "cexpr_emit.hpp"
+#include <libmodcc/export.hpp>
 
-std::string emit_gpu_cpp_source(const Module& m, const printer_options& opt);
-std::string emit_gpu_cu_source(const Module& m, const printer_options& opt);
+ARB_LIBMODCC_API std::string emit_gpu_cpp_source(const Module& m, const printer_options& opt);
+ARB_LIBMODCC_API std::string emit_gpu_cu_source(const Module& m, const printer_options& opt);
 
-class GpuPrinter: public CPrinter {
+class ARB_LIBMODCC_API GpuPrinter: public CPrinter {
 public:
     GpuPrinter(std::ostream& out): CPrinter(out) {}
 
diff --git a/modcc/printer/infoprinter.cpp b/modcc/printer/infoprinter.cpp
index d0b207d63e1b7ac704b54932489ee8858d26a80d..0db24b4990e3a18919dc3ee19945031d57769844 100644
--- a/modcc/printer/infoprinter.cpp
+++ b/modcc/printer/infoprinter.cpp
@@ -17,7 +17,7 @@
 
 using io::quote;
 
-std::string build_info_header(const Module& m, const printer_options& opt, bool cpu, bool gpu) {
+ARB_LIBMODCC_API std::string build_info_header(const Module& m, const printer_options& opt, bool cpu, bool gpu) {
     using io::indent;
     using io::popindent;
 
diff --git a/modcc/printer/infoprinter.hpp b/modcc/printer/infoprinter.hpp
index ae43c8994dd93f659af23c780fd7afa7700f7c1d..41f81a0c4b5fe760586d28d4c64cb249397a2eaf 100644
--- a/modcc/printer/infoprinter.hpp
+++ b/modcc/printer/infoprinter.hpp
@@ -3,9 +3,11 @@
 #include <string>
 
 #include "module.hpp"
+#include <libmodcc/export.hpp>
+
 #include "printer/printeropt.hpp"
 
 // Build header file comprising mechanism metadata
 // and declarations of backend-specific mechanism implementations.
 
-std::string build_info_header(const Module& m, const printer_options& opt, bool cpu=false, bool gpu=false);
+ARB_LIBMODCC_API std::string build_info_header(const Module& m, const printer_options& opt, bool cpu=false, bool gpu=false);
diff --git a/modcc/printer/printerutil.cpp b/modcc/printer/printerutil.cpp
index 7e7b7c31a8b9e8326c7fdaf037531bddbed68811..72f208e99899d79d1de0ee6b489f36f8ebd4c62f 100644
--- a/modcc/printer/printerutil.cpp
+++ b/modcc/printer/printerutil.cpp
@@ -6,7 +6,7 @@
 #include "module.hpp"
 #include "printerutil.hpp"
 
-std::vector<std::string> namespace_components(const std::string& ns) {
+ARB_LIBMODCC_API std::vector<std::string> namespace_components(const std::string& ns) {
     static std::regex ns_regex("([^:]+)(?:::|$)");
 
     std::vector<std::string> components;
@@ -18,7 +18,7 @@ std::vector<std::string> namespace_components(const std::string& ns) {
     return components;
 }
 
-std::vector<LocalVariable*> indexed_locals(scope_ptr scope) {
+ARB_LIBMODCC_API std::vector<LocalVariable*> indexed_locals(scope_ptr scope) {
     std::vector<LocalVariable*> vars;
     for (auto& entry: scope->locals()) {
         LocalVariable* local = entry.second->is_local_variable();
@@ -29,7 +29,7 @@ std::vector<LocalVariable*> indexed_locals(scope_ptr scope) {
     return vars;
 }
 
-std::vector<LocalVariable*> pure_locals(scope_ptr scope) {
+ARB_LIBMODCC_API std::vector<LocalVariable*> pure_locals(scope_ptr scope) {
     std::vector<LocalVariable*> vars;
     for (auto& entry: scope->locals()) {
         LocalVariable* local = entry.second->is_local_variable();
@@ -40,7 +40,7 @@ std::vector<LocalVariable*> pure_locals(scope_ptr scope) {
     return vars;
 }
 
-std::vector<ProcedureExpression*> normal_procedures(const Module& m) {
+ARB_LIBMODCC_API std::vector<ProcedureExpression*> normal_procedures(const Module& m) {
     std::vector<ProcedureExpression*> procs;
 
     for (auto& sym: m.symbols()) {
@@ -54,7 +54,7 @@ std::vector<ProcedureExpression*> normal_procedures(const Module& m) {
     return procs;
 }
 
-public_variable_ids_t public_variable_ids(const Module& m) {
+ARB_LIBMODCC_API public_variable_ids_t public_variable_ids(const Module& m) {
     public_variable_ids_t ids;
     ids.state_ids = m.state_block().state_variables;
 
@@ -79,7 +79,7 @@ public_variable_ids_t public_variable_ids(const Module& m) {
     return ids;
 }
 
-module_variables_t local_module_variables(const Module& m) {
+ARB_LIBMODCC_API module_variables_t local_module_variables(const Module& m) {
     module_variables_t mv;
 
     for (auto& sym: m.symbols()) {
@@ -92,7 +92,7 @@ module_variables_t local_module_variables(const Module& m) {
     return mv;
 }
 
-std::vector<ProcedureExpression*> module_normal_procedures(const Module& m) {
+ARB_LIBMODCC_API std::vector<ProcedureExpression*> module_normal_procedures(const Module& m) {
     std::vector<ProcedureExpression*> procs;
     for (auto& sym: m.symbols()) {
         auto p = sym.second->is_procedure();
@@ -104,17 +104,17 @@ std::vector<ProcedureExpression*> module_normal_procedures(const Module& m) {
     return procs;
 }
 
-APIMethod* find_api_method(const Module& m, const char* which) {
+ARB_LIBMODCC_API APIMethod* find_api_method(const Module& m, const char* which) {
     auto it = m.symbols().find(which);
     return  it==m.symbols().end()? nullptr: it->second->is_api_method();
 }
 
-NetReceiveExpression* find_net_receive(const Module& m) {
+ARB_LIBMODCC_API NetReceiveExpression* find_net_receive(const Module& m) {
     auto it = m.symbols().find("net_receive");
     return it==m.symbols().end()? nullptr: it->second->is_net_receive();
 }
 
-PostEventExpression* find_post_event(const Module& m) {
+ARB_LIBMODCC_API PostEventExpression* find_post_event(const Module& m) {
     auto it = m.symbols().find("post_event");
     return it==m.symbols().end()? nullptr: it->second->is_post_event();
 }
@@ -135,7 +135,7 @@ std::string indexed_variable_info::outer_index_var() const {
     }
 }
 
-indexed_variable_info decode_indexed_variable(IndexedVariable* sym) {
+ARB_LIBMODCC_API indexed_variable_info decode_indexed_variable(IndexedVariable* sym) {
     indexed_variable_info v;
     v.node_index_var = "node_index";
     v.index_var_kind = index_kind::node;
diff --git a/modcc/printer/printerutil.hpp b/modcc/printer/printerutil.hpp
index 77daace53832fe08bdfba2dfc8aca8f24fd83012..608a231ad8afd5027bb9de8334868c64806da2be 100644
--- a/modcc/printer/printerutil.hpp
+++ b/modcc/printer/printerutil.hpp
@@ -11,8 +11,9 @@
 #include "error.hpp"
 #include "expression.hpp"
 #include "module.hpp"
+#include <libmodcc/export.hpp>
 
-std::vector<std::string> namespace_components(const std::string& qualified_namespace);
+ARB_LIBMODCC_API std::vector<std::string> namespace_components(const std::string& qualified_namespace);
 
 // Can use this in a namespace. No __ allowed anywhere, neither _[A-Z], and in _global namespace_ _ followed by anything is verboten.
 const static std::string pp_var_pfx = "_pp_var_";
@@ -79,17 +80,17 @@ inline void assert_has_scope(Expression* expr, const std::string& context) {
 // Scope query functions:
 
 // All local variables in scope with `is_indexed()` true.
-std::vector<LocalVariable*> indexed_locals(scope_ptr scope);
+ARB_LIBMODCC_API std::vector<LocalVariable*> indexed_locals(scope_ptr scope);
 
 // All local variables in scope with `is_arg()` and `is_indexed()` false.
-std::vector<LocalVariable*> pure_locals(scope_ptr scope);
+ARB_LIBMODCC_API std::vector<LocalVariable*> pure_locals(scope_ptr scope);
 
 
 // Module state query functions:
 
 // Normal (not API, net_receive) procedures in module:
 
-std::vector<ProcedureExpression*> normal_procedures(const Module&);
+ARB_LIBMODCC_API std::vector<ProcedureExpression*> normal_procedures(const Module&);
 
 struct public_variable_ids_t {
     std::vector<Id> state_ids;
@@ -99,7 +100,7 @@ struct public_variable_ids_t {
 
 // Public module variables by role.
 
-public_variable_ids_t public_variable_ids(const Module&);
+ARB_LIBMODCC_API public_variable_ids_t public_variable_ids(const Module&);
 
 struct module_variables_t {
     std::vector<VariableExpression*> scalars;
@@ -108,21 +109,21 @@ struct module_variables_t {
 
 // Scalar and array variables with local linkage.
 
-module_variables_t local_module_variables(const Module&);
+ARB_LIBMODCC_API module_variables_t local_module_variables(const Module&);
 
 // "normal" procedures in a module.
 // A normal procedure is one that has been declared with the
 // PROCEDURE keyword in NMODL.
 
-std::vector<ProcedureExpression*> module_normal_procedures(const Module& m);
+ARB_LIBMODCC_API std::vector<ProcedureExpression*> module_normal_procedures(const Module& m);
 
 // Extract key procedures from module.
 
-APIMethod* find_api_method(const Module& m, const char* which);
+ARB_LIBMODCC_API APIMethod* find_api_method(const Module& m, const char* which);
 
-NetReceiveExpression* find_net_receive(const Module& m);
+ARB_LIBMODCC_API NetReceiveExpression* find_net_receive(const Module& m);
 
-PostEventExpression* find_post_event(const Module& m);
+ARB_LIBMODCC_API PostEventExpression* find_post_event(const Module& m);
 
 // For generating vectorized code for reading and writing data sources.
 // node: The data source uses the CV index which is categorized into
@@ -139,7 +140,7 @@ enum class index_kind {
     none
 };
 
-struct indexed_variable_info {
+struct ARB_LIBMODCC_API indexed_variable_info {
     std::string data_var;
     std::string node_index_var;
     std::string cell_index_var;
@@ -157,7 +158,7 @@ struct indexed_variable_info {
     std::string outer_index_var() const;
 };
 
-indexed_variable_info decode_indexed_variable(IndexedVariable* sym);
+ARB_LIBMODCC_API indexed_variable_info decode_indexed_variable(IndexedVariable* sym);
 
 template<typename C>
 size_t emit_array(std::ostream& out, const C& vars) {
diff --git a/modcc/solvers.cpp b/modcc/solvers.cpp
index 4e49ded06562b73ca100e431fdb6f004cd2ef83a..1d31e55d8fe70e4573efed3390b0b389ebf477d0 100644
--- a/modcc/solvers.cpp
+++ b/modcc/solvers.cpp
@@ -964,7 +964,7 @@ public:
     }
 };
 
-expression_ptr remove_unused_locals(BlockExpression* block) {
+ARB_LIBMODCC_API expression_ptr remove_unused_locals(BlockExpression* block) {
     UnusedVisitor unused_visitor;
     block->accept(&unused_visitor);
 
diff --git a/modcc/solvers.hpp b/modcc/solvers.hpp
index 718c0900ffcf3f4f51a6ad972c4b5337ceddcf7e..719ad0c0e3b12db12a2db2460f84dc0418d29064 100644
--- a/modcc/solvers.hpp
+++ b/modcc/solvers.hpp
@@ -12,8 +12,9 @@
 #include "symdiff.hpp"
 #include "symge.hpp"
 #include "visitor.hpp"
+#include <libmodcc/export.hpp>
 
-expression_ptr remove_unused_locals(BlockExpression* block);
+ARB_LIBMODCC_API expression_ptr remove_unused_locals(BlockExpression* block);
 
 class SolverVisitorBase: public BlockRewriterBase {
 protected:
@@ -56,7 +57,7 @@ public:
     }
 };
 
-class CnexpSolverVisitor : public SolverVisitorBase {
+class ARB_LIBMODCC_API CnexpSolverVisitor : public SolverVisitorBase {
 public:
     using SolverVisitorBase::visit;
 
@@ -67,7 +68,7 @@ public:
     virtual void visit(AssignmentExpression *e) override;
 };
 
-class SystemSolver {
+class ARB_LIBMODCC_API SystemSolver {
 protected:
     // Symbolic matrix for backwards Euler step.
     symge::sym_matrix A_;
@@ -132,7 +133,7 @@ public:
 
 };
 
-class SparseSolverVisitor : public SolverVisitorBase {
+class ARB_LIBMODCC_API SparseSolverVisitor : public SolverVisitorBase {
 protected:
     solverVariant solve_variant_;
 
@@ -185,7 +186,7 @@ public:
     }
 };
 
-class SparseNonlinearSolverVisitor : public SolverVisitorBase {
+class ARB_LIBMODCC_API SparseNonlinearSolverVisitor : public SolverVisitorBase {
 protected:
     // 'Current' differential equation is for variable with this
     // index in `dvars`.
@@ -231,7 +232,7 @@ public:
     }
 };
 
-class LinearSolverVisitor : public SolverVisitorBase {
+class ARB_LIBMODCC_API LinearSolverVisitor : public SolverVisitorBase {
 protected:
     // 'Current' differential equation is for variable with this
     // index in `dvars`.
diff --git a/modcc/symdiff.cpp b/modcc/symdiff.cpp
index db32ac3aeb96ffeedfc4fbfaff1996e26229f813..4946a1b8108c4ce3f33a405a712be82269019c8e 100644
--- a/modcc/symdiff.cpp
+++ b/modcc/symdiff.cpp
@@ -87,13 +87,13 @@ private:
     bool found_ = false;
 };
 
-bool involves_identifier(Expression* e, const identifier_set& ids) {
+ARB_LIBMODCC_API bool involves_identifier(Expression* e, const identifier_set& ids) {
     FindIdentifierVisitor v(ids);
     e->accept(&v);
     return v.found();
 }
 
-bool involves_identifier(Expression* e, const std::string& id) {
+ARB_LIBMODCC_API bool involves_identifier(Expression* e, const std::string& id) {
     identifier_set ids = {id};
     FindIdentifierVisitor v(ids);
     e->accept(&v);
@@ -262,7 +262,7 @@ private:
     std::string id_;
 };
 
-double expr_value(Expression* e) {
+ARB_LIBMODCC_API double expr_value(Expression* e) {
     return e && e->is_number()? e->is_number()->value(): NAN;
 }
 
@@ -547,14 +547,14 @@ public:
     }
 };
 
-expression_ptr constant_simplify(Expression* e) {
+ARB_LIBMODCC_API expression_ptr constant_simplify(Expression* e) {
     ConstantSimplifyVisitor csimp_visitor;
     e->accept(&csimp_visitor);
     return csimp_visitor.result();
 }
 
 
-expression_ptr symbolic_pdiff(Expression* e, const std::string& id) {
+ARB_LIBMODCC_API expression_ptr symbolic_pdiff(Expression* e, const std::string& id) {
     if (!involves_identifier(e, id)) {
         return make_expression<NumberExpression>(e->location(), 0);
     }
@@ -644,7 +644,7 @@ private:
     const substitute_map& sub_;
 };
 
-expression_ptr substitute(Expression* e, const std::string& id, Expression* sub) {
+ARB_LIBMODCC_API expression_ptr substitute(Expression* e, const std::string& id, Expression* sub) {
     substitute_map subs;
     subs[id] = sub->clone();
     SubstituteVisitor sub_visitor(subs);
@@ -652,13 +652,13 @@ expression_ptr substitute(Expression* e, const std::string& id, Expression* sub)
     return sub_visitor.result();
 }
 
-expression_ptr substitute(Expression* e, const substitute_map& sub) {
+ARB_LIBMODCC_API expression_ptr substitute(Expression* e, const substitute_map& sub) {
     SubstituteVisitor sub_visitor(sub);
     e->accept(&sub_visitor);
     return sub_visitor.result();
 }
 
-linear_test_result linear_test(Expression* e, const std::vector<std::string>& vars) {
+ARB_LIBMODCC_API linear_test_result linear_test(Expression* e, const std::vector<std::string>& vars) {
     linear_test_result result;
     auto loc = e->location();
     auto zero = [loc]() { return make_expression<IntegerExpression>(loc, 0); };
diff --git a/modcc/symdiff.hpp b/modcc/symdiff.hpp
index 9e8192150d88e670f0d321b49949755ac534c7ba..cb58ffae803c36d37a4bef86e2b6974b9424badf 100644
--- a/modcc/symdiff.hpp
+++ b/modcc/symdiff.hpp
@@ -13,19 +13,20 @@
 #include <utility>
 
 #include "expression.hpp"
+#include <libmodcc/export.hpp>
 
 
 // True if `id` matches the spelling of any identifier in the expression.
-bool involves_identifier(Expression* e, const std::string& id);
+ARB_LIBMODCC_API bool involves_identifier(Expression* e, const std::string& id);
 
 using identifier_set = std::vector<std::string>;
-bool involves_identifier(Expression* e, const identifier_set& ids);
+ARB_LIBMODCC_API bool involves_identifier(Expression* e, const identifier_set& ids);
 
 // Return new expression formed by folding constants and removing trivial terms.
-expression_ptr constant_simplify(Expression* e);
+ARB_LIBMODCC_API expression_ptr constant_simplify(Expression* e);
 
 // Extract value of expression that is a NumberExpression, or else return NAN.
-double expr_value(Expression* e);
+ARB_LIBMODCC_API double expr_value(Expression* e);
 
 // Test if expression is a NumberExpression with value zero.
 inline bool is_zero(Expression* e) {
@@ -33,14 +34,14 @@ inline bool is_zero(Expression* e) {
 }
 
 // Return new expression of symbolic partial differentiation of argument wrt `id`.
-expression_ptr symbolic_pdiff(Expression* e, const std::string& id);
+ARB_LIBMODCC_API expression_ptr symbolic_pdiff(Expression* e, const std::string& id);
 
 // Substitute all occurances of identifier `id` within expression by a clone of `sub`.
 // (Only applicable to unary, binary, call and number expressions.)
-expression_ptr substitute(Expression* e, const std::string& id, Expression* sub);
+ARB_LIBMODCC_API expression_ptr substitute(Expression* e, const std::string& id, Expression* sub);
 
 using substitute_map = std::map<std::string, expression_ptr>;
-expression_ptr substitute(Expression* e, const substitute_map& sub);
+ARB_LIBMODCC_API expression_ptr substitute(Expression* e, const substitute_map& sub);
 
 // Convenience interfaces for the above functions work with `expression_ptr` as
 // well as with `Expression*` values.
@@ -112,7 +113,7 @@ struct linear_test_result {
     }
 };
 
-linear_test_result linear_test(Expression* e, const std::vector<std::string>& vars);
+ARB_LIBMODCC_API linear_test_result linear_test(Expression* e, const std::vector<std::string>& vars);
 
 inline linear_test_result linear_test(const expression_ptr& e, const std::vector<std::string>& vars) {
     return linear_test(e.get(), vars);
diff --git a/modcc/symge.cpp b/modcc/symge.cpp
index 0968b0406f9ce171e50b8fa18f315771596cb214..ccc2afade9201c53b5aed57153b5a4f2661a9a51 100644
--- a/modcc/symge.cpp
+++ b/modcc/symge.cpp
@@ -81,7 +81,7 @@ double estimate_cost(const sym_matrix& A, pivot p) {
 // that are symbols that are either primitive, or defined (in the symbol
 // table) as products or differences of products of other symbols.
 // Returns a vector of vectors of symbols, partitioned by row of the matrix
-std::vector<std::vector<symge::symbol>> gj_reduce(sym_matrix& A, symbol_table& table) {
+ARB_LIBMODCC_API std::vector<std::vector<symge::symbol>> gj_reduce(sym_matrix& A, symbol_table& table) {
     std::vector<std::vector<symge::symbol>> row_symbols;
 
     if (A.nrow()>A.ncol()) throw std::runtime_error("improper matrix for reduction");
diff --git a/modcc/symge.hpp b/modcc/symge.hpp
index 9343609255a8e4a6371345d312f1d4f68760b4da..3a3cbc1ecd10aa129db66411d901abad3bdb00b5 100644
--- a/modcc/symge.hpp
+++ b/modcc/symge.hpp
@@ -3,13 +3,14 @@
 #include <stdexcept>
 
 #include "msparse.hpp"
+#include <libmodcc/export.hpp>
 
 // Symbolic sparse matrix manipulation for symbolic Gauss-Jordan elimination
 // (used in `sparse` solver).
 
 namespace symge {
 
-struct symbol_error: public std::runtime_error {
+struct ARB_LIBMODCC_API symbol_error: public std::runtime_error {
     symbol_error(const std::string& what): std::runtime_error(what) {}
 };
 
@@ -154,6 +155,6 @@ using sym_matrix = msparse::matrix<symbol>;
 // pivots taken from the diagonal elements. New symbol definitions due to fill-in
 // will be added via the provided symbol table.
 // Returns a vector of vectors of symbols, partitioned by row of the matrix
-std::vector<std::vector<symge::symbol>> gj_reduce(sym_matrix& A, symbol_table& table);
+ARB_LIBMODCC_API std::vector<std::vector<symge::symbol>> gj_reduce(sym_matrix& A, symbol_table& table);
 
 } // namespace symge
diff --git a/modcc/token.cpp b/modcc/token.cpp
index e884bc0d062f899fbe0664f689722ff7686f4d28..7ba2f71b9ba508c55e9e5b5dc73416e1d22e5227 100644
--- a/modcc/token.cpp
+++ b/modcc/token.cpp
@@ -157,7 +157,7 @@ static TokenString token_strings[] = {
 
 /// set up lookup tables for converting between tokens and their
 /// string representations
-void initialize_token_maps() {
+ARB_LIBMODCC_API void initialize_token_maps() {
     // ensure that tables are initialized only once
     std::lock_guard<std::mutex> g(mutex);
 
@@ -181,18 +181,18 @@ void initialize_token_maps() {
     }
 }
 
-std::string token_string(tok token) {
+ARB_LIBMODCC_API std::string token_string(tok token) {
     auto pos = token_map.find(token);
     return pos==token_map.end() ? std::string("<unknown token>") : pos->second;
 }
 
-bool is_keyword(Token const& t) {
+ARB_LIBMODCC_API bool is_keyword(Token const& t) {
     for(Keyword *k=keywords; k->name!=nullptr; ++k)
         if(t.type == k->type)
             return true;
     return false;
 }
 
-std::ostream& operator<< (std::ostream& os, Token const& t) {
+ARB_LIBMODCC_API std::ostream& operator<< (std::ostream& os, Token const& t) {
     return os << "<<" << token_string(t.type) << ", " << t.spelling << ", " << t.location << ">>";
 }
diff --git a/modcc/token.hpp b/modcc/token.hpp
index 5e6466fe55853510a27fa9df685c31f67416f1b0..0e09c385b3b323d4e3d2df6c9de225dd5621ea98 100644
--- a/modcc/token.hpp
+++ b/modcc/token.hpp
@@ -5,6 +5,7 @@
 #include <unordered_map>
 
 #include "location.hpp"
+#include <libmodcc/export.hpp>
 
 enum class tok {
     eof, // end of file
@@ -118,8 +119,8 @@ extern std::unordered_map<std::string, tok> keyword_map;
 // for stringifying a token type
 extern std::map<tok, std::string> token_map;
 
-void initialize_token_maps();
-std::string token_string(tok token);
-bool is_keyword(Token const& t);
-std::ostream& operator<< (std::ostream& os, Token const& t);
+ARB_LIBMODCC_API void initialize_token_maps();
+ARB_LIBMODCC_API std::string token_string(tok token);
+ARB_LIBMODCC_API bool is_keyword(Token const& t);
+ARB_LIBMODCC_API std::ostream& operator<< (std::ostream& os, Token const& t);
 
diff --git a/python/test/cpp/CMakeLists.txt b/python/test/cpp/CMakeLists.txt
index fc460292f5aa08c6c8d07250b3f8f8bce277cef3..823a2d816984c0d1a972b3f78cbd124fa74ec706 100644
--- a/python/test/cpp/CMakeLists.txt
+++ b/python/test/cpp/CMakeLists.txt
@@ -9,7 +9,7 @@ set(py_unit_sources
 add_executable(py_unit EXCLUDE_FROM_ALL ${py_unit_sources})
 add_dependencies(tests py_unit)
 
-add_library(py_unit_lib $<TARGET_OBJECTS:pyarb_obj>)
+add_library(py_unit_lib STATIC $<TARGET_OBJECTS:pyarb_obj>)
 target_link_libraries(py_unit_lib PRIVATE arbor pybind11::module)
 
 target_compile_options(py_unit PRIVATE ${ARB_CXX_FLAGS_TARGET_FULL})
diff --git a/sup/CMakeLists.txt b/sup/CMakeLists.txt
index 596888bbc6b9e9a4a8a2fa1369255b0288eaaccb..831b3c544ef1c3aad7fe999695e0c84a3dcb6fd1 100644
--- a/sup/CMakeLists.txt
+++ b/sup/CMakeLists.txt
@@ -12,7 +12,12 @@ target_compile_options(arbor-sup PRIVATE ${ARB_CXX_FLAGS_TARGET_FULL})
 # The sup library uses both the json library and libarbor
 target_link_libraries(arbor-sup PUBLIC ${json_library_name} arbor)
 
-target_include_directories(arbor-sup PUBLIC include)
+#target_include_directories(arbor-sup PUBLIC include)
+target_include_directories(arbor-sup
+    PUBLIC
+        $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>/include
+        $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/include>)
 
 set_target_properties(arbor-sup PROPERTIES OUTPUT_NAME arborsup)
 
+export_visibility(arbor-sup)
diff --git a/sup/include/sup/ioutil.hpp b/sup/include/sup/ioutil.hpp
index cf516dc0eaed61f33129c70154b67a7a4c87f2db..c1be394b6b02862657b151f2421dad6727ee3fa4 100644
--- a/sup/include/sup/ioutil.hpp
+++ b/sup/include/sup/ioutil.hpp
@@ -14,10 +14,12 @@
 #include <iostream>
 #include <fstream>
 
+#include <sup/export.hpp>
 #include <sup/path.hpp>
 
 namespace sup {
 
+
 template <typename charT, typename traitsT = std::char_traits<charT> >
 class basic_null_streambuf: public std::basic_streambuf<charT, traitsT> {
 private:
@@ -42,7 +44,7 @@ protected:
     }
 };
 
-class mask_stream {
+class ARB_SUP_API mask_stream {
 public:
     explicit mask_stream(bool mask): mask_(mask) {}
 
@@ -87,7 +89,7 @@ private:
     bool mask_;
 };
 
-std::fstream open_or_throw(const sup::path& p, std::ios_base::openmode, bool exclusive);
+ARB_SUP_API std::fstream open_or_throw(const sup::path& p, std::ios_base::openmode, bool exclusive);
 
 inline std::fstream open_or_throw(const sup::path& p, bool exclusive) {
     using std::ios_base;
diff --git a/sup/include/sup/json_meter.hpp b/sup/include/sup/json_meter.hpp
index 76c94ae83436d5b2f750bf309ad226b0d472da9b..a017c2643009cef235c5d711bba0f9ea770f8e92 100644
--- a/sup/include/sup/json_meter.hpp
+++ b/sup/include/sup/json_meter.hpp
@@ -1,8 +1,9 @@
 #include <arbor/profile/meter_manager.hpp>
+#include <sup/export.hpp>
 #include <nlohmann/json.hpp>
 
 namespace sup {
 
-nlohmann::json to_json(const arb::profile::meter_report&);
+ARB_SUP_API nlohmann::json to_json(const arb::profile::meter_report&);
 
 } // namespace sup
diff --git a/sup/include/sup/path.hpp b/sup/include/sup/path.hpp
index 3c13aa2d440fdb0d47e02106278c86aae3362018..4ffd7861f2ccc7570f97f83c32f526c670ad8644 100644
--- a/sup/include/sup/path.hpp
+++ b/sup/include/sup/path.hpp
@@ -26,6 +26,8 @@
 #include <utility>
 #include <vector>
 
+#include <sup/export.hpp>
+
 namespace sup {
 
 class posix_path {
@@ -363,8 +365,8 @@ private:
 
 // POSIX implementations of path queries (see path.cpp for implementations).
 
-file_status posix_status(const path&, std::error_code&) noexcept;
-file_status posix_symlink_status(const path&, std::error_code&) noexcept;
+ARB_SUP_API file_status posix_status(const path&, std::error_code&) noexcept;
+ARB_SUP_API file_status posix_symlink_status(const path&, std::error_code&) noexcept;
 
 inline file_status status(const path& p, std::error_code& ec) noexcept {
     return posix_status(p, ec);
@@ -599,7 +601,7 @@ inline constexpr bool operator!=(directory_options a, unsigned x) {
 
 struct posix_directory_state;
 
-struct posix_directory_iterator {
+struct ARB_SUP_API posix_directory_iterator {
     using value_type = directory_entry;
     using difference_type = std::ptrdiff_t;
     using pointer = const directory_entry*;
diff --git a/sup/ioutil.cpp b/sup/ioutil.cpp
index 4725cc1a98be251d978266b783e7f91ee2545775..e8d7ce0e21dfa1fb5fa4dd5da43245bb28116e15 100644
--- a/sup/ioutil.cpp
+++ b/sup/ioutil.cpp
@@ -7,7 +7,7 @@
 
 namespace sup {
 
-std::fstream open_or_throw(const path& p, std::ios_base::openmode mode, bool exclusive) {
+ARB_SUP_API std::fstream open_or_throw(const path& p, std::ios_base::openmode mode, bool exclusive) {
     if (exclusive && exists(p)) {
         throw std::runtime_error(strsub("file % already exists", p));
     }
diff --git a/sup/json_meter.cpp b/sup/json_meter.cpp
index c587d4dfe32b5eebb5a6d1d3522e03fe97be3948..bb40bd48bd1e9fd5dac83f5e579a24d17c4e29a8 100644
--- a/sup/json_meter.cpp
+++ b/sup/json_meter.cpp
@@ -1,4 +1,5 @@
 #include <arbor/profile/meter_manager.hpp>
+#include <sup/json_meter.hpp>
 #include <nlohmann/json.hpp>
 
 namespace sup {
@@ -16,7 +17,7 @@ static nlohmann::json to_json(const arb::profile::measurement& mnt) {
     };
 }
 
-nlohmann::json to_json(const arb::profile::meter_report& report) {
+ARB_SUP_API nlohmann::json to_json(const arb::profile::meter_report& report) {
     nlohmann::json json_meters;
     for (const auto& mnt: report.meters) {
         json_meters.push_back(to_json(mnt));
diff --git a/sup/path.cpp b/sup/path.cpp
index 5622f687b3b7b0cca53387ef1da2f6a809331f2d..d12946ee3b76c069a1a6f6f2e746c9e22ec5c6fa 100644
--- a/sup/path.cpp
+++ b/sup/path.cpp
@@ -52,13 +52,13 @@ namespace impl {
 } // namespace impl
 
 
-file_status posix_status(const path& p, std::error_code& ec) noexcept {
+ARB_SUP_API file_status posix_status(const path& p, std::error_code& ec) noexcept {
     struct stat st;
     int r = stat(p.c_str(), &st);
     return impl::status(p.c_str(), r, st, ec);
 }
 
-file_status posix_symlink_status(const path& p, std::error_code& ec) noexcept {
+ARB_SUP_API file_status posix_symlink_status(const path& p, std::error_code& ec) noexcept {
     struct stat st;
     int r = lstat(p.c_str(), &st);
     return impl::status(p.c_str(), r, st, ec);
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index db3e8e7e92833b809a182a2a3c42257770097c58..5b398e61cb66ac39d1954bf4c7fc0a4e3d4c2769 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -1,6 +1,7 @@
 find_package(Threads REQUIRED)
 
-add_library(gtest EXCLUDE_FROM_ALL STATIC gtest-all.cpp)
+add_library(gtest EXCLUDE_FROM_ALL gtest-all.cpp)
+set_target_properties(gtest PROPERTIES CXX_VISIBILITY_PRESET hidden)
 target_include_directories(gtest PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
 target_link_libraries(gtest PUBLIC Threads::Threads)
 
diff --git a/test/unit/test_fvm_layout.cpp b/test/unit/test_fvm_layout.cpp
index e7836e77e5787375b829cfaed90b159779e8b7b3..e26c8db8a8f765ff0ea8f9774ab89f7f4a445c49 100644
--- a/test/unit/test_fvm_layout.cpp
+++ b/test/unit/test_fvm_layout.cpp
@@ -40,6 +40,9 @@ using util::value_by_key;
 using backend = arb::multicore::backend;
 using fvm_cell = arb::fvm_lowered_cell_impl<backend>;
 
+// instantiate template class
+template class arb::fvm_lowered_cell_impl<arb::multicore::backend>;
+
 namespace {
     struct system {
         std::vector<soma_cell_builder> builders;
diff --git a/test/unit/test_mechcat.cpp b/test/unit/test_mechcat.cpp
index 431dffb08d339dc74f3fbb866f5a9d1043b08f55..181c203476bf73868add8ee28d8b3ecabe59ee89 100644
--- a/test/unit/test_mechcat.cpp
+++ b/test/unit/test_mechcat.cpp
@@ -281,7 +281,15 @@ TEST(mechcat, names) {
 #ifdef USE_DYNAMIC_CATALOGUES
 TEST(mechcat, loading) {
     EXPECT_THROW(load_catalogue(LIBDIR "/does-not-exist-catalogue.so"), file_not_found_error);
+#if defined(ARB_ARBOR_SHARED_LIBRARY)
+#if defined(ARB_ON_MACOS)
+    EXPECT_THROW(load_catalogue(LIBDIR "/libarbor.dylib"), bad_catalogue_error);
+#else
+    EXPECT_THROW(load_catalogue(LIBDIR "/libarbor.so"), bad_catalogue_error);
+#endif
+#else
     EXPECT_THROW(load_catalogue(LIBDIR "/libarbor.a"), bad_catalogue_error);
+#endif
     const mechanism_catalogue* cat = nullptr;
     EXPECT_NO_THROW(cat = &load_catalogue(LIBDIR "/dummy-catalogue.so"));
     ASSERT_NE(cat, nullptr);