diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index f1e3d8e4f2..4718d5e0bd 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -58,7 +58,8 @@ jobs:
# two statements: a substitution inside echo would hide the script's exit code
run: |
set -eu
- matrix=$(python3 ci/ci_matrix.py build '${{ github.event_name }}')
+ # TEMP: one cell only - win64 Release, the lane standalone_capi_dasbind fails on
+ matrix='{"include": [{"target": "windows", "architecture": 64, "cmake_preset": "Release", "sanitizers": "none", "release_target": "windows", "release_arch": "x86_64", "runner": "windows-latest", "architecture_string": "x64", "archive_ext": "zip", "llvm_disabled": "ON", "jit_disabled": "ON", "build_system": "cmake", "cmake_generator": "Ninja"}]}'
echo "matrix=$matrix" >> "$GITHUB_OUTPUT"
- name: Cache LLVM
@@ -294,7 +295,9 @@ jobs:
# daslang's MSVC debug info is /Z7 (CMakeCommon.txt) so sccache
# can cache it.
cmake --no-warn-unused-cli -B./build -G "${{ matrix.cmake_generator }}" -DCMAKE_BUILD_TYPE:STRING=${{ matrix.cmake_preset }} -DCMAKE_C_COMPILER_LAUNCHER=sccache -DCMAKE_CXX_COMPILER_LAUNCHER=sccache -DDAS_LLVM_DISABLED=${{ env.das_llvm_disabled }} -DCMAKE_TOOLCHAIN_FILE="$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake"
- cmake --build ./build --config ${{ matrix.cmake_preset }} --parallel
+ # TEMP: the debug step needs daslang and the fixture DLL, nothing else.
+ # Building ALL here costs about an hour of AOT translation units.
+ cmake --build ./build --config ${{ matrix.cmake_preset }} --parallel --target daslang standalone_init_fixture_shared
;;
linux_arm*)
CC=clang CXX=clang++ cmake --no-warn-unused-cli -B./build -DCMAKE_BUILD_TYPE:STRING=${{ matrix.cmake_preset }} -DDAS_GLFW_DISABLED=ON -DDAS_HV_DISABLED=OFF -DDAS_SQLITE_DISABLED=OFF -DDAS_LLVM_DISABLED=${{ env.das_llvm_disabled }} -G \
@@ -332,6 +335,7 @@ jobs:
# warm sequential sweep, so a separate prewarm only duplicated the
# frontend-compile of every test. The target still exists for local use.
- name: "Test"
+ if: false # TEMP: debug run only needs the DEBUG step
run: |
set -eux
# When daslang is sanitizer-instrumented, its JIT-emitted .dll cache
@@ -410,7 +414,94 @@ jobs:
cmake --build ./build --config ${{ matrix.cmake_preset }} --target run_tests_interpreter || cmake --build ./build --config ${{ matrix.cmake_preset }} --target run_tests_interpreter_isolated
;;
esac
+ - name: "TEMP DEBUG: standalone_capi_dasbind on win64"
+ if: always()
+ shell: bash
+ run: |
+ set -x
+ H=tests-cpp/big/standalone_ctx
+ D=build/tests-cpp/big/standalone_ctx
+ BIN=bin/daslang.exe
+
+ echo "===== which CRT does each side link ====="
+ dumpbin //dependents "$BIN" || true
+ dumpbin //dependents "$D/standalone_init_fixture.dll" || true
+
+ echo "===== 0: bindings required, nothing called ====="
+ cat > "$H/_dbg0.das" <<'EOS'
+ options gen2
+ require _standalone_init_fixture_c
+ [export]
+ def main : int {
+ print("loaded, no call\n")
+ return 0
+ }
+ EOS
+ "$BIN" "$H/_dbg0.das" || true; echo "dbg0 exit=$?"
+
+ echo "===== 1: create only, no destroy ====="
+ cat > "$H/_dbg1.das" <<'EOS'
+ options gen2
+ require _standalone_init_fixture_c
+ [export]
+ def main : int {
+ print("before create\n")
+ var ctx = standalone_init_fixture_create()
+ print("created null={ctx == null}\n")
+ return 0
+ }
+ EOS
+ "$BIN" "$H/_dbg1.das" || true; echo "dbg1 exit=$?"
+
+ echo "===== 2: create + destroy ====="
+ cat > "$H/_dbg2.das" <<'EOS'
+ options gen2
+ require _standalone_init_fixture_c
+ [export]
+ def main : int {
+ var ctx = standalone_init_fixture_create()
+ print("created null={ctx == null}\n")
+ standalone_init_fixture_destroy(ctx)
+ print("destroyed\n")
+ return 0
+ }
+ EOS
+ "$BIN" "$H/_dbg2.das" || true; echo "dbg2 exit=$?"
+
+ echo "===== 3: create + scalar call + destroy ====="
+ cat > "$H/_dbg3.das" <<'EOS'
+ options gen2
+ require _standalone_init_fixture_c
+ [export]
+ def main : int {
+ var ctx = standalone_init_fixture_create()
+ let first = standalone_init_fixture_get_first(ctx)
+ print("get_first={first}\n")
+ standalone_init_fixture_destroy(ctx)
+ print("destroyed\n")
+ return 0
+ }
+ EOS
+ "$BIN" "$H/_dbg3.das" || true; echo "dbg3 exit=$?"
+
+ echo "===== 4: the real host, with its output ====="
+ "$BIN" "$H/test_standalone_bindings_host.das" || true; echo "host exit=$?"
+
+ echo "===== 4b: a stack for the create crash, under cdb ====="
+ CDB="/c/Program Files (x86)/Windows Kits/10/Debuggers/x64/cdb.exe"
+ ls "$CDB" || CDB=$(find "/c/Program Files (x86)/Windows Kits" -name cdb.exe 2>/dev/null | head -1)
+ if [ -n "$CDB" ] && [ -x "$CDB" ]; then
+ "$CDB" -g -G -c 'g; .lastevent; k 40; !heap -p -a @rcx; q' \
+ "$(cygpath -w "$PWD/bin/daslang.exe")" "$(cygpath -w "$PWD/$H/_dbg1.das")" 2>&1 | tail -80 || true
+ else
+ echo "no cdb on this runner"
+ fi
+
+ echo "===== 5: through ctest, for the record ====="
+ ctest --test-dir build --build-config Release -R standalone_capi_dasbind --output-on-failure || true
+
- name: "Small C++ Tests"
+ if: false # TEMP: debug run only needs the DEBUG step
run: |
set -eux
# Suppress LSan false-positives from the Uri binding; see "Test" step above
@@ -437,7 +528,7 @@ jobs:
# per-PR ctest is -L small, so only this step executes a generated
# context. memory_model_4gb allocates a real 4 GB chunk and stays
# local-only.
- if: matrix.cmake_preset == 'Release' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
+ if: false # TEMP: debug run only needs the DEBUG step
run: |
set -eux
case "${{ matrix.target }}${{ matrix.architecture }}" in
@@ -466,9 +557,7 @@ jobs:
# duplicates the Release/Debug compile and interpreter coverage on every
# PR. Keep it on the canonical nightly and explicit full-workflow runs.
needs: pre_job
- if: >-
- (github.event_name == 'schedule' && github.repository == 'GaijinEntertainment/daScript')
- || github.event_name == 'workflow_dispatch'
+ if: false # TEMP: win64 debug run
runs-on: windows-latest
permissions:
contents: read
@@ -560,6 +649,7 @@ jobs:
run: cmake --build ./build --config RelWithDebInfo --target run_tests_interpreter
- name: "Small C++ Tests"
+ if: false # TEMP: debug run only needs the DEBUG step
run: ctest --test-dir build --build-config RelWithDebInfo -L small --output-on-failure
###########################################################
@@ -570,9 +660,7 @@ jobs:
# The regular matrix cell is the fast LLVM-free MSVC gate. Its scheduled
# run still owns the full AOT suite, so do not duplicate that sweep here.
needs: pre_job
- if: >-
- (github.event_name == 'schedule' && github.repository == 'GaijinEntertainment/daScript')
- || github.event_name == 'workflow_dispatch'
+ if: false # TEMP: win64 debug run
runs-on: windows-latest
permissions:
contents: read
@@ -652,6 +740,7 @@ jobs:
cmake --build ./build --config Release --target run_tests_interpreter || cmake --build ./build --config Release --target run_tests_interpreter_isolated
- name: "Small C++ Tests"
+ if: false # TEMP: debug run only needs the DEBUG step
run: ctest --test-dir build --build-config Release -L small --output-on-failure
###########################################################
@@ -668,9 +757,7 @@ jobs:
# Per-PR/push lane and manual dispatch, plus the nightly cron on the canonical repo: the cron
# is what seeds this job's sccache slot below (a master push run is usually pre_job-skipped
# as a concurrent duplicate of the PR that just merged, so its save rarely lands).
- if: >-
- (github.event_name == 'schedule' && github.repository == 'GaijinEntertainment/daScript')
- || (github.event_name != 'schedule' && needs.pre_job.outputs.should_skip != 'true')
+ if: false # TEMP: win64 debug run
runs-on: ubuntu-latest
# actions: write needed for `gh cache delete` in the sccache refresh step.
permissions:
@@ -778,7 +865,7 @@ jobs:
# scheduled cron runs the toolchains only on the canonical repo, so forks
# don't run (and fail) the nightly — that would email every fork owner.
# Manual workflow_dispatch still runs them anywhere.
- if: (github.event_name == 'schedule' && github.repository == 'GaijinEntertainment/daScript') || github.event_name == 'workflow_dispatch'
+ if: false # TEMP: win64 debug run
runs-on: windows-latest
defaults:
run:
@@ -867,6 +954,7 @@ jobs:
|| cmake --build ./build-mingw --config Release --target run_tests_jit_isolated
- name: "Small C++ Tests"
+ if: false # TEMP: debug run only needs the DEBUG step
run: |
set -eux
cd build-mingw
@@ -935,7 +1023,7 @@ jobs:
# scheduled cron runs the toolchains only on the canonical repo, so forks
# don't run (and fail) the nightly — that would email every fork owner.
# Manual workflow_dispatch still runs them anywhere.
- if: (github.event_name == 'schedule' && github.repository == 'GaijinEntertainment/daScript') || github.event_name == 'workflow_dispatch'
+ if: false # TEMP: win64 debug run
runs-on: windows-latest
steps:
- name: "SCM Checkout"
@@ -1037,7 +1125,7 @@ jobs:
# (core + HV/SQLITE/Audio/PUGIXML/UnitTest modules) is covered without it.
###########################################################
needs: pre_job
- if: needs.pre_job.outputs.should_skip != 'true' && github.event_name != 'schedule'
+ if: false # TEMP: win64 debug run
runs-on: ubuntu-latest
steps:
- name: "SCM Checkout"
diff --git a/.github/workflows/build_eastl.yml b/.github/workflows/build_eastl.yml
index 1e05e61814..936a0b920a 100644
--- a/.github/workflows/build_eastl.yml
+++ b/.github/workflows/build_eastl.yml
@@ -1,9 +1,8 @@
name: build_eastl
on:
- push:
- branches: [master]
- pull_request:
+ # TEMP: push trigger off for the win64 debug run
+ # TEMP: pull_request trigger off for the win64 debug run
workflow_dispatch:
# Compile-coverage for the dagor config: EASTL containers, dag::Vector as
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index f0df555278..a3d9655fb6 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -8,22 +8,8 @@ name: "CodeQL"
# CodeQL — that surface is covered by the in-tree lint.
on:
- push:
- branches: [master]
- paths:
- - 'src/**'
- - 'include/**'
- - 'modules/**'
- - 'tests-cpp/**'
- - '.github/workflows/codeql.yml'
- pull_request:
- branches: [master]
- paths:
- - 'src/**'
- - 'include/**'
- - 'modules/**'
- - 'tests-cpp/**'
- - '.github/workflows/codeql.yml'
+ # TEMP: push trigger off for the win64 debug run
+ # TEMP: pull_request trigger off for the win64 debug run
schedule:
# weekly full refresh keeps the master baseline current even when no
# C++-touching push happens (PR alert diffing compares against it)
diff --git a/.github/workflows/cpp_mcp_release.yml b/.github/workflows/cpp_mcp_release.yml
index a52ebd45fc..83b46b6bd9 100644
--- a/.github/workflows/cpp_mcp_release.yml
+++ b/.github/workflows/cpp_mcp_release.yml
@@ -15,15 +15,7 @@ name: cpp-mcp release
# modules, so every external module (dasHV/OpenSSL, GLFW, Audio, ...) is disabled
# — no vcpkg/openssl/apt module deps, and a ~360-step build instead of 2000+.
on:
- pull_request:
- paths:
- - 'utils/mcp/**'
- - 'utils/common/**'
- - 'tree-sitter-daslang/*.yml' # ast-grep rule files the bundle ships
- - 'ci/make_cpp_mcp_bundle.sh'
- - 'ci/smoke_test_cpp_mcp.sh'
- - '.github/workflows/cpp_mcp_release.yml'
- - 'CMakeLists.txt'
+ # TEMP: pull_request trigger off for the win64 debug run
workflow_dispatch:
release:
types: [prereleased]
diff --git a/.github/workflows/dasllama-server-e2e.yml b/.github/workflows/dasllama-server-e2e.yml
index 188fcea64a..8b38f0aa66 100644
--- a/.github/workflows/dasllama-server-e2e.yml
+++ b/.github/workflows/dasllama-server-e2e.yml
@@ -4,17 +4,8 @@ name: dasllama-server control page e2e
# fixtures; regeneration rail: utils/dasllama-server/tests/fixtures/README.md.
on:
- pull_request:
- paths:
- - 'utils/dasllama-server/control.html'
- - 'utils/dasllama-server/tests/**'
- - '.github/workflows/dasllama-server-e2e.yml'
- push:
- branches-ignore: [master]
- paths:
- - 'utils/dasllama-server/control.html'
- - 'utils/dasllama-server/tests/**'
- - '.github/workflows/dasllama-server-e2e.yml'
+ # TEMP: pull_request trigger off for the win64 debug run
+ # TEMP: push trigger off for the win64 debug run
workflow_dispatch:
jobs:
diff --git a/.github/workflows/dasllama_server_release.yml b/.github/workflows/dasllama_server_release.yml
index dde839a49c..672162143c 100644
--- a/.github/workflows/dasllama_server_release.yml
+++ b/.github/workflows/dasllama_server_release.yml
@@ -23,18 +23,7 @@ name: dasllama-server release
# it, and 22.04's (glibc 2.35) admits Debian 12 and every later Ubuntu, where 24.04's (2.39)
# refused Debian 12 with `GLIBC_2.38 not found`.
on:
- push:
- branches-ignore: [master]
- paths:
- - '.github/workflows/dasllama_server_release.yml'
- - 'utils/dasllama-server/**'
- - 'utils/watchdog/**'
- - 'utils/daspkg/**'
- - 'daslib/daspkg.das'
- - 'modules/dasHV/**'
- - 'modules/dasLLAMA/benchmarks/lcpp_bench.das'
- - 'modules/dasLLAMA/dasllama/dasllama_bench.das'
- - '!**/*.md'
+ # TEMP: push trigger off for the win64 debug run
workflow_dispatch:
inputs:
publish:
diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml
index 7738fb4b6b..c7f326f597 100644
--- a/.github/workflows/doc.yml
+++ b/.github/workflows/doc.yml
@@ -4,23 +4,8 @@ name: Build doc
on:
release:
types: prereleased
- push:
- paths:
- - 'doc/**'
- - 'daslib/**'
- - 'src/builtin/**'
- - 'modules/dasLLAMA/dasllama/**'
- - 'modules/dasImgui/**'
- - 'modules/dasVulkan/**'
- pull_request:
- paths:
- - 'doc/**'
- - 'daslib/**'
- - 'src/builtin/**'
- - 'modules/dasLLAMA/dasllama/**'
- - 'modules/dasImgui/**'
- - 'modules/dasVulkan/**'
-
+ # TEMP: push trigger off for the win64 debug run
+ # TEMP: pull_request trigger off for the win64 debug run
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
diff --git a/.github/workflows/extended_checks.yml b/.github/workflows/extended_checks.yml
index 0c4b729fce..91078f94bd 100644
--- a/.github/workflows/extended_checks.yml
+++ b/.github/workflows/extended_checks.yml
@@ -1,9 +1,8 @@
name: extended checks
on:
- push:
- branches: [master]
- pull_request:
+ # TEMP: push trigger off for the win64 debug run
+ # TEMP: pull_request trigger off for the win64 debug run
# 04:00 UTC — after build.yml (02:00), the daspkg index (02:30) and imgui (03:00).
# The nightly is what SEEDS the sccache slots below. Their save is master-only,
# but the master PUSH run is usually skipped by pre_job as a concurrent duplicate
diff --git a/.github/workflows/fatman.yml b/.github/workflows/fatman.yml
index dcfd6a8ab5..2274b22021 100644
--- a/.github/workflows/fatman.yml
+++ b/.github/workflows/fatman.yml
@@ -7,14 +7,7 @@
name: fatman
on:
- pull_request:
- paths:
- - 'CMakeLists.txt'
- - 'web/CMakeLists.txt'
- - 'examples/fatman/**'
- - '.github/workflows/fatman.yml'
- - 'include/**'
- - 'src/**'
+ # TEMP: pull_request trigger off for the win64 debug run
workflow_dispatch:
concurrency:
diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml
index 0528be01e4..5894bc8db0 100644
--- a/.github/workflows/pages.yml
+++ b/.github/workflows/pages.yml
@@ -1,8 +1,7 @@
name: Deploy daslang.io
on:
- push:
- branches: [master]
+ # TEMP: push trigger off for the win64 debug run
workflow_dispatch:
# Only one site deployment at a time
diff --git a/.github/workflows/playground-e2e.yml b/.github/workflows/playground-e2e.yml
index 6ccdc7bacc..2979d078d9 100644
--- a/.github/workflows/playground-e2e.yml
+++ b/.github/workflows/playground-e2e.yml
@@ -6,17 +6,8 @@ name: Playground e2e (no WASM)
# job wasm_specs).
on:
- pull_request:
- paths:
- - 'site/**'
- - 'web/examples/ui/**'
- - '.github/workflows/playground-e2e.yml'
- push:
- branches-ignore: [master]
- paths:
- - 'site/**'
- - 'web/examples/ui/**'
- - '.github/workflows/playground-e2e.yml'
+ # TEMP: pull_request trigger off for the win64 debug run
+ # TEMP: push trigger off for the win64 debug run
workflow_dispatch:
jobs:
diff --git a/.github/workflows/vulkan_checks.yml b/.github/workflows/vulkan_checks.yml
index f5c2c1d07c..5d998981e5 100644
--- a/.github/workflows/vulkan_checks.yml
+++ b/.github/workflows/vulkan_checks.yml
@@ -6,9 +6,7 @@ name: dasVulkan checks
# and the module-wide lint. The full render suite is nightly_vulkan.yml.
on:
- pull_request:
- paths:
- - 'modules/dasVulkan/**'
+ # TEMP: pull_request trigger off for the win64 debug run
workflow_dispatch:
concurrency:
diff --git a/.github/workflows/wasm_build.yml b/.github/workflows/wasm_build.yml
index db44fe83c0..a3cab28cb1 100644
--- a/.github/workflows/wasm_build.yml
+++ b/.github/workflows/wasm_build.yml
@@ -1,9 +1,8 @@
name: wasm_build
on:
- push:
- branches: [master]
- pull_request:
+ # TEMP: push trigger off for the win64 debug run
+ # TEMP: pull_request trigger off for the win64 debug run
workflow_dispatch:
concurrency:
diff --git a/.github/workflows/wasmboy.yml b/.github/workflows/wasmboy.yml
index c396619aa2..d968060b4a 100644
--- a/.github/workflows/wasmboy.yml
+++ b/.github/workflows/wasmboy.yml
@@ -10,14 +10,7 @@
name: wasmboy
on:
- pull_request:
- paths:
- - 'CMakeLists.txt'
- - 'web/**'
- - 'examples/fatman/**'
- - '.github/workflows/wasmboy.yml'
- - 'include/**'
- - 'src/**'
+ # TEMP: pull_request trigger off for the win64 debug run
workflow_dispatch:
concurrency:
diff --git a/.gitignore b/.gitignore
index 6782e960da..0c6e6b85d8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -48,6 +48,11 @@ modules/**/*.shared_module
examples/**/*.shared_module
_aot_generated/
_llvm_aot_generated/
+# the -ctx driver writes each fixture's daslang bindings beside the host that requires them
+tests-cpp/big/standalone_ctx/_*_c.das
+_standalone_ctx_generated/
+# the -lib suite generates each fixture's daslang bindings beside its host
+tests/jit_tests/_*_c.das
.vscode/
.cache/
@@ -199,3 +204,7 @@ modules/dasLLAMA/benchmarks/asr/_pybench_rows.txt
# python bytecode, anywhere
__pycache__/
site/files/profile_results_*.json
+
+# examples/c_api_library builds its three libraries here, and CMake generates their bindings
+examples/c_api_library/_out/
+examples/c_api_library/*_c.das
diff --git a/CLAUDE.md b/CLAUDE.md
index e58d49517f..b5317876c0 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -98,7 +98,7 @@ Task-specific instructions are split into skill files under `skills/`. You MUST
| `skills/internal/documentation_rst.md` | Editing RST in `doc/source/`, `//!` doc-comments in `daslib/*.das`, tutorial RST pages |
| `skills/internal/tutorials.md` | Anything that looks like a tutorial - they live under `/tutorials//`, NEVER `modules//tutorial/` |
| `skills/internal/tutorial_prose.md` | WRITING or revising general-reader doc/tutorial prose (`documentation_rst.md` is mechanics, this is the words) |
-| `skills/cpp_integration.md` | Embedding daslang in C++; binding types/functions/enums; shipping without the compiler (`libDaScriptNano`, or a standalone context on the full runtime) |
+| `skills/cpp_integration.md` | Embedding daslang in C++; binding types/functions/enums; shipping without the compiler (`libDaScriptNano`, or a standalone context on the full runtime); calling daslang from C (`daslang -lib`) |
| `skills/internal/cpp_codebase_notes.md` | Working on daslang's own C++ - where inference/builtins/errors/parser live, AST function flags |
| `skills/internal/clang_bind_build.md` | Enabling `dasClangBind` / bumping the libclang SDK / running any `bind_*.das` self-binder |
| `skills/daslib_modules.md` | Working with `daslib/` modules or extending the stdlib |
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 55e991d21e..0f245dcf9c 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1645,6 +1645,9 @@ if (NOT ${DAS_TOOLS_DISABLED})
# their C++, which is why they live here and not beside the nano library.
add_subdirectory(examples/standalone)
+ # `daslang -lib` libraries bound back into daslang through generated [extern] bindings.
+ add_subdirectory(examples/c_api_library)
+
endif()
# This list should be significantly reduced, most of the files, except aot related should be private.
diff --git a/daslib/ARCHITECTURE.md b/daslib/ARCHITECTURE.md
index ae4c6e5510..6de07e7cda 100644
--- a/daslib/ARCHITECTURE.md
+++ b/daslib/ARCHITECTURE.md
@@ -3,10 +3,11 @@
Design rationale a maintainer cannot recover from the code alone. One numbered section per
module; entries are anchored to symbols.
-Three companions carry a concern each; a section number is unique across all four files.
+Four companions carry a concern each; a section number is unique across all five files.
- `ARCHITECTURE_LINT.md` - sec. 1-4: perf_lint, lint_config, lint, style_lint.
- `ARCHITECTURE_EMIT.md` - sec. 5-7, 28-29: aot_cpp, aot_standalone, flatten, the shader rails.
+- `ARCHITECTURE_CAPI.md` - sec. 30: c_api_header, the C surface both backends emit.
- `ARCHITECTURE_LINQ.md` - sec. 11-17, 33, 37: the linq family, sql_linq, sql_migrate.
## 8. ast_verify
diff --git a/daslib/ARCHITECTURE_CAPI.md b/daslib/ARCHITECTURE_CAPI.md
new file mode 100644
index 0000000000..896c24862f
--- /dev/null
+++ b/daslib/ARCHITECTURE_CAPI.md
@@ -0,0 +1,92 @@
+# daslib architecture notes - the generated C API
+
+Companion to `ARCHITECTURE.md` in this folder; section numbers are unique across the family.
+The header this section describes is the one BOTH backends write: `aot_standalone` for a
+standalone context, `inject_lib` for `daslang -lib`.
+
+## 30. c_api_header
+
+- **One module writes every generated header, and one module owns the C declarations in it.** The
+ C declarations come first; the C++ half (`CppApi`) follows under `#ifdef __cplusplus`, and both
+ hosts include one file without seeing the other's half. `aot_standalone` emits the matching
+ `extern "C"` bodies and `inject_lib` the matching thunks, both off this describer, so JIT and
+ AOT cannot disagree about what crosses or what it is called.
+- **C sits ON TOP of C++, not under it, and only in the AOT tier.** A standalone context's C++
+ methods are DEFINED in the generated `.cpp` and call the AOT functions directly; each
+ `extern "C"` entry point is a wrapper over the method beside it. So the C++ API is the wide one -
+ every export, in native C++ types, spelled by `aot_standalone` because it owns C++ types the way
+ this module owns C ones - and C is the subset C can express, not a ceiling over it. A method
+ keeps the daslang name: `[export_c(name = ...)]` renames the C symbol only. `daslang -lib` cannot
+ follow, because a jitted library has no C++ source to put underneath: its C++ half stays inline
+ proxies over C, so the two tiers' C++ APIs differ ON PURPOSE, and only their C surface is twinned.
+- **The generated `.cpp` carries the header's text instead of including it.** The header is the
+ HOST's file - it may move it or edit it - and the implementation must not be breakable that way.
+ Types are declared once (`type_defs` never reaches the bodies), so inlining is a substitution for
+ the include rather than a second copy. A host header that drifts from the library still fails
+ loudly: a changed or removed signature is a link error, a changed layout trips the header's own
+ size and offset asserts.
+- **`Function.flags.exports` is the whole selection truth.** `[export_c]`
+ (`ExportCAnnotation`, `daslib/export_c.das` - a das `[function_macro]`, so the C surface is
+ decided entirely in daslang) sets it, and
+ `policies.export_public_functions` sets it for every public entry-module function under
+ `-lib-export-all` (`MarkSymbolUse::exportPublicFunctions`, `src/ast/ast_export.cpp`). The
+ selection forms are therefore one bit read several ways: `-lib` alone accepts only what carries
+ the annotation, `--jit-lib-export-marked` and a standalone context accept the bit however it was
+ set (so `[export]` selects), and `-lib-export-all` is that same acceptance plus the marking
+ policy. Accepting the bit however set is what licenses skipping an unspellable signature with a
+ warning; one that ASKED for C with `[export_c]` and cannot cross is an error. Whether a signature
+ CAN cross is decided here, after infer, because argument types do not exist at annotation time.
+- **`[export_c]` reaches a library source with no `require` because `daslib/export_c` is
+ `!inscope`.** That marker sets `visibleEverywhere`, which `Module::isVisibleDirectly` honors
+ ahead of the require map - the mechanism that makes `daslib/builtin.das` universal. Being visible
+ still needs the module LOADED, and `daslib/just_in_time.das` - injected whenever the JIT is on -
+ requires it, so `-lib`, `-jit` and `-exe` carry the annotation for free; a compile with no JIT
+ needs `require daslib/export_c`. It lives in its own module rather than in `c_api_header` because
+ that require costs `ast_boost` alone, not the header emitter's whole graph.
+- **Refusal is per stage, not per module**: `collect_c_exports` returns its rejections and logs
+ its skips. An `[export_c]` that cannot cross comes back for the caller to report - `macro_error`
+ during compilation, the jit error log during codegen - so this module needs no `ProgramPtr` and
+ no reporting policy of its own.
+- **The scalar widths and the vector layouts are C++-side facts this emitter mirrors.**
+ `bool` is one byte (the `static_assert(sizeof(bool)==1)` in `getTypeBaseSize`,
+ `src/simulate/debug_info.cpp`), so das `bool` meets C as `bool`. `float3` is `{x, y, z}` at
+ 12 bytes and 4-byte alignment, because `vec3` (`include/daScript/misc/vectypes.h`)
+ is a plain three-field struct with no `alignas` - the 16-byte vec4f shape is the JIT's
+ register ABI, not the memory layout a header has to mirror. So no vector or structure carries
+ an alignment attribute, and every declared struct carries a size assert plus one offset assert
+ per field. `Structure.sizeOf` is already rounded to the struct's alignment
+ (`Structure::getSizeOf`, `src/ast/ast.cpp`), so `sizeof` in C matches it directly.
+- **A bound value type crosses as the wrap type its annotation carries.** A
+ `ManagedValueAnnotation` is not a ref type and holds `makeValueType()` - the `WrapType::type`
+ das moves the value through, reachable from das as `get_underlying_value_type`. The header
+ typedefs that shape under the das type's name and the body assigns the das type through it; a C++
+ host still gets the real type, because the C++ half carries the module's `aotRequire` include. It
+ is the one C type with an alignment attribute - a handle's alignment is not its wrap type's
+ (`BigEntityId` is 16-aligned, four floats are 4-aligned) - on ONE declarator, since the attribute
+ applies per declarator. Size and alignment are both asserted. A ref-type handle stays `void *`.
+- **An enumeration is a typedef of its base integer plus loose enumerators, never a C
+ `enum`** - a C enum's underlying type is implementation-defined, which would break the
+ size assert on the 8/16/64-bit bases and on negative values. The values are read off the
+ entry's folded constant, so no smart pointer is needed to reach `find_enum_value`.
+- **Types are emitted only when a signature reaches them, in post-order.** A by-value
+ field's structure is defined before the structure holding it; every structure also gets a
+ forward typedef ahead of all definitions, which is what lets a self-referential
+ (`Node?`) field compile. A pointer's target only has to be NAMEABLE, so one whose fields C
+ cannot spell is forward-declared and never DEFINED - defining it emits fields with no type at
+ all - while a representable target is defined, so a host can read through the pointer. A cycle
+ terminates either way, and a pointer to something C cannot even name degrades to `void *`
+ rather than refusing the function.
+- **The same describer writes the daslang twin of the header.** `build_das_bindings`
+ (`--jit-lib-bindings`) emits one `[extern(cdecl, late, ...)]` per entry point plus a das struct
+ per crossing structure, so a das host requires what the generator wrote instead of hand-writing
+ declarations or parsing the C header through clang. `late` is what lets the file compile before
+ the library exists, which is what a build system needs to generate it as an ordinary output. It
+ re-exports `dasbind` (`require dasbind public`), because the declarations land in that module and
+ a host requiring only the bindings would not see them. Types are spelled as das names them -
+ `float3`, `range`, `int` for an enumeration - so a host passes `safe_addr` of the real type and
+ writes no `reinterpret`; a type das cannot restate as the C ABI carries it degrades that one
+ entry point to a comment, not the file. Only `-lib` writes bindings: a standalone context emits
+ `extern "C"` bodies for the host to compile IN, with no export decoration, so a shared library
+ built from them exports nothing for dasbind to load.
+- **A `fixed_array` argument crosses (as `const T *`, which the das ABI already passes) but a
+ `fixed_array` result does not** - that is a CMRES of an array, a pointer nothing in C sizes.
diff --git a/daslib/ARCHITECTURE_EMIT.md b/daslib/ARCHITECTURE_EMIT.md
index cc91418de8..99cf48777a 100644
--- a/daslib/ARCHITECTURE_EMIT.md
+++ b/daslib/ARCHITECTURE_EMIT.md
@@ -52,8 +52,30 @@ Companion to `ARCHITECTURE.md` in this folder; section numbers are unique across
dasHV takes `rtti_core`). A module missing from the daslib list still registers, only in
the dependencies-first pass that follows; a module added to the C++ side joins the list.
+- **The AnnotationInfo table resets at the START of the debug-info dump, not its end.** The
+ globals' `VarInfo`s are written after that dump and a handled global's info refers to an
+ `AnnotationInfo` by the name the dump minted, so clearing on the way out left `&` with nothing
+ after it. Only that walk reaches a global's annotation - `writeHandledAnnotations` iterates
+ types, structs and functions.
+- **A member pointer is qualified with `aotModuleName`, never the raw module name.** The main
+ module is unnamed, so `_module.name` is empty for every type a script declares itself, while
+ `describeCppType` resolves those types through `g_aot_main_module_name`. Where one emitter writes
+ both - `das_safe_navigation` - they must agree, or the type argument names the
+ context's namespace while the member pointer names nothing. Only a standalone context sets a
+ main-module name, so regular AOT never sees it.
+
## 6. aot_standalone
+- **The entry module's structures are visited SORTED.** `visitModule` takes `sortStructures`,
+ which runs `topoSortStructures` so a by-value field's structure is complete before the structure
+ holding it; regular AOT passes it through `visit(program, adapter, true)` and a standalone
+ context, which visits the entry module by itself, has to ask for it too. Declaration order is the
+ author's, and nothing else re-derives it.
+- **The context name is an identifier; the file stem is not the same string.** A stem reaches C++
+ as a namespace and C as a symbol prefix, so `while.das` or `3d-math.das` would open
+ `namespace while {`. `context_name` is the stem through `cpp_context_ident`, while `file_stem`
+ keeps the raw name - a build system predicts the generated file names from the input path and
+ cannot be told they were sanitized.
- **The generated constructor IS the init protocol** - a standalone context never calls
`Context::runInitScript`, so the ctor reproduces its observable semantics inline:
`memset(context.globals, 0, context.getGlobalSize())` mirrors runInitScript's globals
diff --git a/daslib/aot_standalone.das b/daslib/aot_standalone.das
index d1e4bfca6a..c1bb4d4cf6 100644
--- a/daslib/aot_standalone.das
+++ b/daslib/aot_standalone.das
@@ -15,24 +15,11 @@ require daslib/functional
require daslib/ast_print_flags
require daslib/aot_constants
require daslib/aot_cpp
+require daslib/c_api_header
options strict_smart_pointers = false
-struct StandaloneContextCfg {
- //! Configuration for standalone context generation.
- context_name : string;
- class_name : string;
- cpp_output_dir : string;
- cross_platform : bool;
- //! the context links C++ modules beyond the builtin one, so its constructor registers them
- registers_modules : bool
-};
-
-def aotFunctionName(str : string) {
- return replace(str, "`", "__")
-}
-
def writeStandaloneContextMethods(var prog : ProgramPtr; var logs : StringBuilderWriter; prefix : string; declare_only : bool; cfg : StandaloneContextCfg) {
let fnn = collectProgramUsedFunctions(prog, false, false);
@@ -43,7 +30,7 @@ def writeStandaloneContextMethods(var prog : ProgramPtr; var logs : StringBuilde
if (declare_only) {
write(logs, " ");
}
- write(logs, "auto {prefix}{aotFunctionName(string(fn.origin != null ? fn.origin.name : fn.name))} ( ");
+ write(logs, "auto {prefix}{c_ident(standalone_function_name(fn))} ( ");
var vars : array
vars |> reserve(length(fn.arguments))
for (variable in fn.arguments) {
@@ -143,6 +130,7 @@ def private makeGlobalVarInfos(program : ProgramPtr; var helper : AotDebugInfoHe
def private writeGlobalVarInfos(var helper : AotDebugInfoHelper?; infos : array>; var tw : StringBuilderWriter) {
for (entry in infos) {
let suffix = "_gvar_{entry.index}"
+ helper->registerHandledAnnotation(unsafe(addr(tw)), entry.info)
helper->writeDim(unsafe(addr(tw)), entry.info, suffix)
helper->writeArgTypes(unsafe(addr(tw)), entry.info, suffix)
helper->writeArgNames(unsafe(addr(tw)), entry.info, suffix)
@@ -291,7 +279,7 @@ def writeStandaloneCtor(cfg : StandaloneContextCfg; initFunctions : string; var
write(tw, " context.tabAdLookup = make_shared>();\n");
program.get_ptr() |> for_each_module_no_order($(pm) {
pm |> for_each_annotation_ordered($(k; v) {
- write(tw, " (*context.tabAdLookup)[{k : x}] = {v};\n");
+ write(tw, " (*context.tabAdLookup)[{k:#x}] = {v};\n");
});
});
@@ -305,18 +293,8 @@ def writeStandaloneCtor(cfg : StandaloneContextCfg; initFunctions : string; var
write(tw, "\}\n");
}
-def writeStandaloneContext(var program : ProgramPtr, initFunctions : string, var header : StringBuilderWriter, var source : StringBuilderWriter; cfg : StandaloneContextCfg; globals : array; var context : Context) {
-
- write(header, "\n\n");
- //! the module scope is the FIRST base: constructed before Context, destroyed after it, so the
- //! registry outlives the context's own teardown
- let bases = cfg.registers_modules ? "public StandaloneModuleScope, public Context" : "public Context"
- write(header, "class {cfg.class_name} : {bases} \{\n");
- write(header, "public:\n");
- write(header, " {cfg.class_name}();\n");
- writeStandaloneContextMethods(program, header, "", true, cfg);
- write(header, "\};\n");
+def writeStandaloneContext(var program : ProgramPtr, initFunctions : string, var source : StringBuilderWriter; cfg : StandaloneContextCfg; globals : array; var context : Context) {
writeStandaloneContextMethods(program, source, "{cfg.class_name}::", false, cfg);
writeStandaloneCtor(cfg, initFunctions, source, program, globals, context);
}
@@ -378,11 +356,8 @@ class StandaloneContextGen : CppAot {
};
-def writeModuleDeclarations(var header, source : StringBuilderWriter; registrations : array) {
- //! the header carries the registry include (the class derives from its scope base); the
- //! source carries the module declarations its registration table takes addresses of
+def writeModuleDeclarations(var source : StringBuilderWriter; registrations : array) {
if (empty(registrations)) return
- write(header, "#include \"daScript/simulate/standalone_modules.h\"\n");
write(source, "#include \"daScript/simulate/standalone_modules.h\"\n");
for (entry in registrations) {
write(source, "DECLARE_MODULE({entry.mod.cppClassName});\n");
@@ -402,8 +377,7 @@ def writeModuleRegistration(var source : StringBuilderWriter; registrations : ar
write(source, "static const bool das_standalone_modules_added = standaloneAddModules(das_standalone_modules, {length(registrations)});\n\n");
}
-def writeRegistration(var header : StringBuilderWriter;
- var source : StringBuilderWriter;
+def writeRegistration(var source : StringBuilderWriter;
initFunctions : string;
var program : ProgramPtr;
cfg : StandaloneContextCfg;
@@ -411,12 +385,10 @@ def writeRegistration(var header : StringBuilderWriter;
globals : array;
var context : Context) {
write(source, "using namespace {program.thisNamespace};\n");
- write(header, "namespace {cfg.context_name} \{\n");
write(source, "namespace {cfg.context_name} \{\n");
dumpRegisterAot(unsafe(addr(source)), program, context, true, cfg.cross_platform);
writeModuleRegistration(source, registrations);
- writeStandaloneContext(program, initFunctions, header, source, cfg, globals, context);
- write(header, "\} // namespace {cfg.context_name}\n");
+ writeStandaloneContext(program, initFunctions, source, cfg, globals, context);
write(source, "\} // namespace {cfg.context_name}\n");
}
@@ -458,7 +430,6 @@ def addFunctionInfo(fnn : array; var helper : AotDebugInfoHelper?) {
def genStandaloneSrc(var program : ProgramPtr;
- var header : StringBuilderWriter;
var source : StringBuilderWriter; cfg : StandaloneContextCfg;
var coll : BlockVariableCollector?; globals : array) {
var initFunctions : string;
@@ -478,7 +449,7 @@ def genStandaloneSrc(var program : ProgramPtr;
var gen = new StandaloneContextGen(program, unsafe(addr(tmp_writer)), coll, cfg.cross_platform);
make_visitor(*gen) $(adapter) {
gen.adapter := adapter
- program |> visit_module(adapter, program.getThisModule);
+ program |> visit_module(adapter, program.getThisModule, true);
// a generic instance several modules instantiated is one C++ function: emit the first copy only
var emitted : table
for (pfun in collectProgramUsedFunctions(program, false, false)) {
@@ -505,20 +476,64 @@ def genStandaloneSrc(var program : ProgramPtr;
}
}
}
- write(header, type_defs);
write(source, "namespace {program.thisNamespace} \{\n");
write(source, "{ctx_generated}");
write(source, "\} // namespace {program.thisNamespace}\n");
- return initFunctions
+ return <- (init_functions = initFunctions, type_defs = type_defs)
+}
+
+def private moduleIsNoAot(var pmod : Module?) : bool {
+ var marked = 0
+ var plain = 0
+ pmod |> for_each_module_function() $(other) {
+ if (other.flags.builtIn) {
+ return
+ }
+ if (other.flags.noAot) {
+ marked++
+ } else {
+ plain++
+ }
+ }
+ return marked > 0 && plain == 0
+}
+
+def private whyNoAot(var pfun : Function?) : string {
+ if ((pfun |> find_annotation("no_aot")) != null) {
+ return "it is annotated [no_aot]"
+ }
+ if (moduleIsNoAot(pfun._module)) {
+ return "module {pfun._module.name} is marked no-AOT as a whole, which a module-level `options no_aot` does"
+ }
+ return "the compiler marked it no-AOT - it uses a construct the C++ emitter declines"
+}
+
+def private declaredNoAot(program : ProgramPtr) : string {
+ var why = ""
+ program.get_ptr() |> for_each_module_no_order($(var pm) {
+ pm |> for_each_module_function() $(var pfun) {
+ if (!pfun.flags.noAot || !is_used(program, pfun)) {
+ return
+ }
+ if ((pfun |> find_annotation("no_aot")) == null && moduleIsNoAot(pm)) {
+ why = "it reaches {pm.name}, a module marked no-AOT as a whole"
+ }
+ }
+ });
+ return why
}
-def private isMarkedNoAot(pfun : Function?) : bool {
- for (ann in pfun.annotations) {
- if (ann.annotation.name == "no_aot") {
- return true
+def private writeStandaloneSkipStub(program : ProgramPtr; cfg : StandaloneContextCfg; why : string) : bool {
+ let cur_mod = program.getThisModule.name.empty() ? cfg.file_stem : string(program.getThisModule.name)
+ to_log(LOG_WARNING, "standalone AOT skips {cur_mod}.das: {why}\n")
+ var ok = true
+ for (ext in ["h", "cpp"]) {
+ fopen("{cfg.cpp_output_dir}/{cfg.file_stem}.das.{ext}", "wb") $(fw) {
+ ok &&= fw != null
+ fwrite(fw, "// {cur_mod}.das has no standalone form: {why}\n") if (fw != null)
}
}
- return false
+ return ok
}
def private checkAllUsedFunctionsCanAot(program : ProgramPtr) {
@@ -528,11 +543,8 @@ def private checkAllUsedFunctionsCanAot(program : ProgramPtr) {
return
}
if (pfun.flags.noAot) {
- if (isMarkedNoAot(pfun)) {
- macro_error(program, pfun.at, "standalone AOT cannot emit function {pfun.name}: it is [no_aot], and a standalone context has no interpreter")
- } else {
- macro_error(program, pfun.at, "standalone AOT cannot emit function {pfun.name}: it uses a type AOT cannot express, and a standalone context has no interpreter")
- }
+ macro_error(program, pfun.at, "standalone AOT cannot emit function {pfun.name}: {whyNoAot(pfun)}, " +
+ "and a standalone context is C++ with no interpreted bodies to fall back to")
}
});
});
@@ -592,57 +604,163 @@ def private prepareProgramForEmission(var program : ProgramPtr; context : Contex
return coll
}
+def private writeCApiBodies(var writer : StringBuilderWriter; var exports : array; names : CNames; cfg : StandaloneContextCfg) {
+ let ns = "das::{cfg.context_name}"
+ let api = "{names.prefix |> to_upper()}_API"
+ write(writer, "\n// C API - the entry points a C host links\n")
+ write(writer, "extern \"C\" \{\n")
+ write(writer, "{api} {names.prefix}_ctx * {names.prefix}_create(void) \{\n")
+ write(writer, " return ({names.prefix}_ctx *) new {ns}::{cfg.class_name}();\n\}\n")
+ write(writer, "{api} void {names.prefix}_destroy({names.prefix}_ctx * ctx) \{\n")
+ write(writer, " delete ({ns}::{cfg.class_name} *) ctx;\n\}\n")
+ write(writer, "{api} const char * {names.prefix}_last_error({names.prefix}_ctx * ctx) \{\n")
+ write(writer, " return ctx ? (({ns}::{cfg.class_name} *) ctx)->getException() : nullptr;\n\}\n")
+ write(writer, "{api} void {names.prefix}_shutdown_runtime(void) \{\n\}\n")
+ for (e in exports) {
+ var fn = e.fn
+ write(writer, "{c_declaration(e, names)} \{\n")
+ var args : array
+ args |> reserve(length(e.params) + 1)
+ for (p, variable in e.params, fn.arguments) {
+ let cpp = describeCppType(variable._type, DescribeConfig(skip_ref = true, cross_platform = cfg.cross_platform))
+ if (p.by_pointer) {
+ args |> push("*({cpp} *) {p.name}")
+ } elif (variable._type.isString) {
+ args |> push("(char *) {p.name}")
+ } elif (variable._type.enumType != null || variable._type.isPointer) {
+ args |> push("({cpp}) {p.name}")
+ } else {
+ args |> push(p.name)
+ }
+ }
+ let arg_list = args |> join(", ")
+ let method = c_ident(standalone_function_name(fn))
+ let call = "(({ns}::{cfg.class_name} *) ctx)->{method}({arg_list})"
+ if (e.result.via_out) {
+ let cpp_res = describeCppType(fn.result, DescribeConfig(skip_ref = true, cross_platform = cfg.cross_platform))
+ write(writer, " *({cpp_res} *) out = {call};\n")
+ } elif (fn.result.isVoid) {
+ write(writer, " {call};\n")
+ } elif (fn.result.enumType != null || fn.result.isPointer) {
+ write(writer, " return ({e.result.c_type}) {call};\n")
+ } else {
+ write(writer, " return {call};\n")
+ }
+ write(writer, "\}\n")
+ }
+ write(writer, "\} // extern \"C\"\n")
+}
+
+
+def private writeDasBindings(var exports : array; names : CNames;
+ cfg : StandaloneContextCfg; cur_mod : string) : bool {
+ if (cfg.das_bindings_path |> empty()) {
+ return true
+ }
+ let lib = cfg.das_bindings_library
+ let b = DasBindings(linux_path = "{lib}.so", macos_path = "{lib}.dylib", windows_path = "{lib}.dll")
+ let by = "daslang utils/aot/main.das -- -ctx {cur_mod}.das {cfg.cpp_output_dir}"
+ return emit_das_bindings(exports, names, b, by, cfg.das_bindings_path)
+}
+
+def private inlined_header(header, already : string) : string {
+ var seen : table
+ for (line in already |> split("\n")) {
+ if (line |> starts_with("#include")) {
+ seen |> insert(line)
+ }
+ }
+ return build_string() $(w) {
+ for (line in header |> split("\n")) {
+ if (line == "#pragma once" || ((line |> starts_with("#include")) && (seen |> key_exists(line)))) {
+ continue
+ }
+ write(w, "{line}\n")
+ }
+ }
+}
+
+
+def private writeStandaloneSource(mod_name, cur_mod : string; modules : array;
+ registrations : array;
+ header_content, bodies : string) : string {
+ return build_string() $(source) {
+ if (!empty(mod_name)) {
+ write(source, "// Module {mod_name}\n")
+ }
+ write(source, "#include \"daScript/simulate/standalone_ctx_utils.h\"\n")
+ write(source, "{join(modules, "")}")
+ writeModuleDeclarations(source, registrations)
+ write(source, "\n// {cur_mod}.das.h, inlined - the host compiles its own copy of this text\n")
+ write(source, inlined_header(header_content, join(modules, "")))
+ write(source, "\n")
+ write(source, AOT_HEADERS)
+ write(source, bodies)
+ write(source, AOT_FOOTER)
+ }
+}
+
+
def public runStandaloneVisitor(var program : ProgramPtr, modules : array; registrations : array; var pctx : smart_ptr; cfg : StandaloneContextCfg) : bool {
//! Runs the standalone AOT visitor on the program to generate C++ source and header files.
//! `registrations` are the C++ modules the generated constructor registers, in order.
//! Returns false (writing nothing) when emission collected errors.
assume context = *pctx;
+ let no_aot = declaredNoAot(program)
+ if (!no_aot.empty()) {
+ return writeStandaloneSkipStub(program, cfg, no_aot)
+ }
+
var coll = prepareProgramForEmission(program, context)
let globals <- usedGlobals(program)
let mod = program.getThisModule;
let mod_name = mod.moduleFlags.promoted ? "" : string(mod.name);
set_aot_main_module_name(cfg.context_name)
- var source_content = ""
- let header_content = build_string() $(header) {
- source_content = build_string() $(source) {
- write(header, "#pragma once\n");
+ let names = CNames(prefix = c_ident_part(cfg.file_stem), this_module = program.getThisModule)
+ var selected <- collect_c_exports(program.get_ptr(), names, true)
+ for (r in selected.errors) {
+ to_log(LOG_ERROR, "{r.message}\n")
+ }
+ var type_defs = ""
+ let bodies = build_string() $(source) {
+ write(source, "namespace das \{\n");
+ let gen <- genStandaloneSrc(program, source, cfg, coll, globals);
+ type_defs = gen.type_defs
+ writeRegistration(source, gen.init_functions, program, cfg, registrations, globals, context);
+ writeCApiBodies(source, selected.exports, names, cfg);
+ write(source, "\} // namespace das\n");
+ }
+ let cur_mod = mod.name.empty() ? cfg.file_stem : string(mod.name);
+ let cpp_includes = build_string() $(inc) {
if (!empty(mod_name)) {
- write(header, "// Module {mod_name}\n");
+ write(inc, "// Module {mod_name}\n")
}
- write(header, "{AOT_INCLUDES}");
- write(header, "{join(modules, "")}");
-
- if (!empty(mod_name)) {
- write(source, "// Module {mod_name}\n");
+ write(inc, "{AOT_INCLUDES}")
+ write(inc, "{join(modules, "")}")
+ if (!empty(registrations)) {
+ write(inc, "#include \"daScript/simulate/standalone_modules.h\"\n")
}
-
- write(source, "#include \"daScript/simulate/standalone_ctx_utils.h\"\n");
- write(source, "{join(modules, "")}");
- writeModuleDeclarations(header, source, registrations);
- write(source, "#include \"{mod.name.empty() ? cfg.context_name : string(mod.name)}.das.h\"\n\n");
- write(source, AOT_HEADERS);
-
- write(source, "namespace das \{\n");
- write(header, "namespace das \{\n");
- let initFunctions = genStandaloneSrc(program, header, source, cfg, coll, globals);
- writeRegistration(header, source, initFunctions, program, cfg, registrations, globals, context);
- write(source, "\} // namespace das\n");
- write(header, "\} // namespace das\n");
-
- write(source, AOT_FOOTER);
-
}
+ let class_decl = build_string() $(decl) {
+ writeStandaloneContextMethods(program, decl, "", true, cfg)
}
+ let cpp_api = CppApi(class_name = cfg.class_name, namespace_name = cfg.context_name,
+ includes = cpp_includes, type_defs = type_defs, class_decl = class_decl,
+ registers_modules = cfg.registers_modules)
+ let header_content = build_standalone_header(selected.exports, names, cpp_api,
+ "daslang utils/aot/main.das -- -ctx {cur_mod}.das {cfg.cpp_output_dir}",
+ "compile the generated .cpp and link it with the daslang runtime")
+ let source_content = writeStandaloneSource(mod_name, cur_mod, modules, registrations, header_content, bodies)
set_aot_main_module_name("")
- if (log_aot_emit_errors(program)) {
+ if (log_aot_emit_errors(program) || !(selected.errors |> empty())
+ || !writeDasBindings(selected.exports, names, cfg, cur_mod)) {
return false
}
- let cur_mod = mod.name.empty() ? cfg.context_name : string(mod.name);
- let outputFile = "{cfg.cpp_output_dir}/{cur_mod}.das";
+ let outputFile = "{cfg.cpp_output_dir}/{cfg.file_stem}.das";
fopen("{outputFile}.h", "wb") $(fw) {
if (fw != null) {
fwrite(fw, header_content)
@@ -666,17 +784,35 @@ def public standalone_aot(input : string; output_dir : string; cross_platform :
return standalone_aot(input, output_dir, cross_platform, paranoid_validation, cop) $(var _program : ProgramPtr) {}
}
+[export]
+def public standalone_aot(input, output_dir, bindings_path, bindings_library : string;
+ cross_platform : bool, paranoid_validation : bool; cop : CodeOfPolicies) {
+ return standalone_aot(input, output_dir, bindings_path, bindings_library,
+ cross_platform, paranoid_validation, cop) $(var _program : ProgramPtr) {}
+}
+
[export, unused_argument(paranoid_validation)]
def public standalone_aot(input : string; output_dir : string; cross_platform : bool, paranoid_validation : bool; cop : CodeOfPolicies; before_emit : block<(var program : ProgramPtr) : void>) {
+ return standalone_aot(input, output_dir, "", "", cross_platform, paranoid_validation, cop, before_emit)
+}
+
+
+[export, unused_argument(paranoid_validation)]
+def public standalone_aot(input, output_dir, bindings_path, bindings_library : string;
+ cross_platform : bool, paranoid_validation : bool; cop : CodeOfPolicies;
+ before_emit : block<(var program : ProgramPtr) : void>) {
//! Compiles a daslang file and generates standalone AOT C++ code in the given output directory.
//! `before_emit` receives the compiled program, so a driver can read annotations
//! without compiling the program a second time.
let file_name = input |> split_by_chars("/\\") |> back()
- let ctx_name = (file_name |> split("."))[0]
- var cfg = StandaloneContextCfg(context_name = ctx_name,
+ let ctx_stem = (file_name |> split("."))[0]
+ var cfg = StandaloneContextCfg(context_name = cpp_context_ident(ctx_stem),
+ file_stem = ctx_stem,
class_name = "Standalone",
cpp_output_dir = output_dir,
- cross_platform = cross_platform)
+ cross_platform = cross_platform,
+ das_bindings_path = bindings_path,
+ das_bindings_library = bindings_library)
var ok = false
using() $(var mg : ModuleGroup) {
var inscope access <- make_file_access("")
diff --git a/daslib/ast_boost.das b/daslib/ast_boost.das
index a300be9e42..5d98c7b153 100644
--- a/daslib/ast_boost.das
+++ b/daslib/ast_boost.das
@@ -925,6 +925,18 @@ def convert_to_expression(value : auto ==const) {
return <- convert_to_expression(value, LineInfo())
}
+def find_annotation(var fn : Function?; ann_name : string) : AnnotationDeclaration? {
+ if (fn == null) {
+ return null
+ }
+ for (ann in fn.annotations) {
+ if (ann != null && ann.annotation.name == ann_name) {
+ return ann
+ }
+ }
+ return null
+}
+
def find_annotation(mod_name, ann_name : string) : Annotation const? {
//! Finds an annotation by module name and annotation name in the compiling program.
var mod = find_compiling_module(mod_name)
diff --git a/daslib/c_api_header.das b/daslib/c_api_header.das
new file mode 100644
index 0000000000..f0e905417c
--- /dev/null
+++ b/daslib/c_api_header.das
@@ -0,0 +1,1133 @@
+options gen2
+options indenting = 4
+
+module c_api_header shared private
+
+require daslib/ast_boost
+require daslib/rtti
+require daslib/fio
+require daslib/strings_boost
+require strings
+
+
+let C_KEYWORDS <- {"restrict", "_Bool", "_Complex", "_Imaginary", "_Alignas", "_Alignof",
+ "_Atomic", "_Generic", "_Noreturn", "_Static_assert", "_Thread_local", "typeof"}
+
+
+struct public StandaloneContextCfg {
+ context_name : string;
+ file_stem : string;
+ class_name : string;
+ cpp_output_dir : string;
+ cross_platform : bool;
+ registers_modules : bool;
+ das_bindings_path : string;
+ das_bindings_library : string
+};
+
+
+def public c_ident_part(name : string) : string {
+ if (name |> empty()) {
+ return ""
+ }
+ var out = build_string() $(writer) {
+ name |> peek_data() $(bytes) {
+ for (b in bytes) {
+ let ch = int(b)
+ let keep = (is_alpha(ch) || is_number(ch) || ch == '_')
+ write_char(writer, keep ? ch : '_')
+ }
+ }
+ }
+ if (is_number(first_character(out))) {
+ out = "_{out}"
+ }
+ return out
+}
+
+
+def public c_ident(name : string) : string {
+ let out = c_ident_part(name)
+ return (is_cpp_keyword(out) || (C_KEYWORDS |> key_exists(out))) ? "{out}_" : out
+}
+
+
+def public cpp_context_ident(stem : string) : string {
+ let id = c_ident(stem)
+ return id |> empty() ? "das_ctx" : "ctx_{id}"
+}
+
+
+def public lib_prefix_from_path(input : string) : string {
+ let stem = input |> base_name() |> split(".")
+ return c_ident_part(stem |> empty() ? "" : stem[0])
+}
+
+
+def public export_c_name_override(fn : Function?) : string {
+ for (ann in fn.annotations) {
+ if (ann.annotation.name != "export_c") {
+ continue
+ }
+ for (arg in ann.arguments) {
+ if (arg.name == "name" && arg.basicType == Type.tString) {
+ return string(arg.sValue)
+ }
+ }
+ }
+ return ""
+}
+
+
+def public has_export_c_annotation(var fn : Function?) : bool {
+ return (fn |> find_annotation("export_c")) != null
+}
+
+
+def private aotFunctionName(str : string) {
+ return replace(str, "`", "__")
+}
+
+def public standalone_function_name(fn : Function?) : string {
+ let renamed = fn |> export_c_name_override()
+ if (!(renamed |> empty())) {
+ return renamed
+ }
+ return aotFunctionName(string(fn.origin != null ? fn.origin.name : fn.name))
+}
+
+
+def public c_export_symbol(prefix : string; var fn : Function?; name_override : string) : string {
+ if (!(name_override |> empty())) {
+ return "{prefix}_{name_override}"
+ }
+ let das_name = string(fn.origin != null ? fn.origin.name : fn.name)
+ return "{prefix}_{c_ident_part(das_name)}"
+}
+
+
+struct public CType {
+ ok : bool
+ spelling : string
+ by_pointer : bool
+ opaque : bool
+}
+
+struct CVector {
+ ok : bool
+ name : string
+ elem : string
+ lanes : int
+ span : bool
+ bytes : int
+}
+
+
+def private is_unsigned_type(t : Type) : bool {
+ return t == Type.tUInt || t == Type.tUInt8 || t == Type.tUInt16 || t == Type.tUInt64
+}
+
+
+def private c_int_name(is_signed : bool; bytes : int) : string {
+ return "{is_signed ? "" : "u"}int{bytes * 8}_t"
+}
+
+
+def private scalar_of(t : TypeDeclPtr) : string {
+ if (t.isBool) {
+ return "bool"
+ }
+ if (t.isInteger) {
+ return c_int_name(t.isSignedInteger, t.sizeOf)
+ }
+ if (t.isBitfield) {
+ return c_int_name(false, t.sizeOf)
+ }
+ if (t.isFloatOrDouble) {
+ return t.sizeOf == 4 ? "float" : "double"
+ }
+ return ""
+}
+
+
+def private vector_of(var t : TypeDeclPtr) : CVector {
+ if (t == null || !t.isVectorType) {
+ return CVector(ok = false)
+ }
+ let lanes = t.vectorDim
+ let bytes = t.sizeOf / lanes
+ if (!t.isRange && (lanes < 2 || lanes > 4 || bytes != 4)) {
+ return CVector(ok = false)
+ }
+ let base = t.isRange ? t.rangeBaseType : t.vectorBaseType
+ let is_unsigned = is_unsigned_type(base)
+ var elem = c_int_name(!is_unsigned, bytes)
+ var name = "{is_unsigned ? "uint" : "int"}{lanes}"
+ if (base == Type.tFloat || base == Type.tDouble) {
+ elem = bytes == 4 ? "float" : "double"
+ name = "{elem}{lanes}"
+ }
+ if (t.isRange) {
+ name = "{is_unsigned ? "u" : ""}range{bytes == 8 ? "64" : ""}"
+ }
+ return CVector(ok = true, name = name, elem = elem, lanes = lanes, span = t.isRange, bytes = t.sizeOf)
+}
+
+
+let VEC_LANE_NAMES <- ["x", "y", "z", "w"]
+let SPAN_LANE_NAMES <- ["from", "to"]
+
+
+def private vector_field_names(v : CVector) : array {
+ return [for (i in range(v.lanes)); v.span ? SPAN_LANE_NAMES[i] : VEC_LANE_NAMES[i]]
+}
+
+
+struct public CNames {
+ prefix : string
+ @do_not_delete this_module : Module?
+}
+
+
+def private c_type_name(names : CNames; var owner : Module?; name : das_string) : string {
+ if (owner == null || owner == names.this_module || owner.name |> empty()) {
+ return "{names.prefix}_{c_ident_part(string(name))}"
+ }
+ return "{names.prefix}_{c_ident_part(string(owner.name))}_{c_ident_part(string(name))}"
+}
+
+
+def private peel(var t : TypeDeclPtr) : TypeDeclPtr {
+ if (t.baseType == Type.tDistinct && t.firstType != null) {
+ return peel(t.firstType)
+ }
+ return t
+}
+
+
+def private is_pod_struct(var st : Structure?) : bool {
+ return st != null && !st.flags.isClass && !(st.fields |> empty())
+}
+
+
+def private pointee_ok(var t : TypeDeclPtr) : bool {
+ if (t == null) {
+ return true
+ }
+ var pt = peel(t)
+ if (pt.isStructure) {
+ return is_pod_struct(pt.structType)
+ }
+ if (pt.isEnumT) {
+ return pt.enumType != null && !pt.enumType.external
+ }
+ return !(scalar_of(pt) |> empty()) || vector_of(pt).ok || pt.isVoid
+}
+
+
+def private pointer_c_type(var t : TypeDeclPtr; names : CNames) : CType {
+ if (t.flags.smartPtr) {
+ return CType(ok = false)
+ }
+ var target = t.firstType
+ if (target == null || target.isVoid || !pointee_ok(target)) {
+ return CType(ok = true, spelling = "void *")
+ }
+ var pt = peel(target)
+ var inner = ""
+ if (pt.isStructure) {
+ inner = c_type_name(names, pt.structType._module, pt.structType.name)
+ } elif (pt.isEnumT) {
+ inner = c_type_name(names, pt.enumType._module, pt.enumType.name)
+ } else {
+ let v = vector_of(pt)
+ inner = v.ok ? "{names.prefix}_{v.name}" : scalar_of(pt)
+ }
+ let qual = pt.flags.constant ? "const " : ""
+ return CType(ok = true, spelling = "{qual}{inner} *")
+}
+
+
+def private field_c_type(var t : TypeDeclPtr; names : CNames; var visiting : table) : CType {
+ var ft = peel(t)
+ if (ft.baseType == Type.tFixedArray) {
+ let inner = ft.firstType == null ? CType(ok = false) : field_c_type(ft.firstType, names, visiting)
+ return inner.ok && !inner.by_pointer ? CType(ok = true, spelling = inner.spelling) : CType(ok = false)
+ }
+ if (ft.isStructure) {
+ if (!struct_c_ok(ft.structType, names, visiting)) {
+ return CType(ok = false)
+ }
+ return CType(ok = true, spelling = c_type_name(names, ft.structType._module, ft.structType.name))
+ }
+ return value_c_type(ft, names)
+}
+
+
+def private struct_c_ok(var st : Structure?; names : CNames; var visiting : table) : bool {
+ if (!is_pod_struct(st)) {
+ return false
+ }
+ let key = "{st._module.name}::{st.name}"
+ if (visiting |> key_exists(key)) {
+ return true
+ }
+ visiting |> insert(key)
+ var ok = true
+ for (fld in st.fields) {
+ if (!field_c_type(fld._type, names, visiting).ok) {
+ ok = false
+ break
+ }
+ }
+ visiting |> erase(key)
+ return ok
+}
+
+
+def private handle_wrap_type(var t : TypeDeclPtr) : TypeDeclPtr {
+ if (t.baseType != Type.tHandle || t.annotation == null || t.annotation.isRefType) {
+ return TypeDeclPtr()
+ }
+ return <- get_underlying_value_type(t)
+}
+
+
+def private handle_c_type(var t : TypeDeclPtr; names : CNames) : CType {
+ var wrap <- handle_wrap_type(t)
+ if (wrap == null) {
+ return CType(ok = false)
+ }
+ let inner = value_c_type(wrap, names)
+ if (!inner.ok) {
+ return CType(ok = false)
+ }
+ return CType(ok = true, spelling = c_type_name(names, t.annotation._module, t.annotation.name),
+ by_pointer = inner.by_pointer)
+}
+
+
+def private value_c_type(var t : TypeDeclPtr; names : CNames) : CType {
+ if (t.baseType == Type.tHandle) {
+ return handle_c_type(t, names)
+ }
+ if (t.isPointer) {
+ return pointer_c_type(t, names)
+ }
+ if (t.isString) {
+ return CType(ok = true, spelling = "const char *")
+ }
+ if (t.isEnumT) {
+ if (t.enumType == null || t.enumType.external) {
+ return CType(ok = false)
+ }
+ return CType(ok = true, spelling = c_type_name(names, t.enumType._module, t.enumType.name))
+ }
+ let v = vector_of(t)
+ if (v.ok) {
+ return CType(ok = true, spelling = "{names.prefix}_{v.name}", by_pointer = true)
+ }
+ let sc = scalar_of(t)
+ return sc |> empty() ? CType(ok = false) : CType(ok = true, spelling = sc)
+}
+
+
+def private opaque_c_of(var t : TypeDeclPtr; names : CNames) : CType {
+ let bt = t.baseType
+ let crosses = (bt == Type.tArray || bt == Type.tTable || bt == Type.tTuple || bt == Type.tVariant
+ || bt == Type.tIterator || bt == Type.tLambda
+ || (bt == Type.tStructure && t.structType != null)
+ || (bt == Type.tHandle && t.annotation != null))
+ if (!crosses) {
+ return CType(ok = false)
+ }
+ let described = t.describe() |> replace(" const", "")
+ let spelling = "{names.prefix}_{c_ident_part(described)}"
+ return CType(ok = true, spelling = spelling, by_pointer = true, opaque = true)
+}
+
+
+def public c_type_of(var t : TypeDeclPtr; names : CNames; allow_opaque : bool = false) : CType {
+ if (t == null) {
+ return CType(ok = false)
+ }
+ var pt = peel(t)
+ if (pt.isVoid) {
+ return CType(ok = true, spelling = "void")
+ }
+ if (pt.isStructure) {
+ var visiting : table
+ if (!struct_c_ok(pt.structType, names, visiting)) {
+ return CType(ok = false)
+ }
+ return CType(ok = true, spelling = c_type_name(names, pt.structType._module, pt.structType.name), by_pointer = true)
+ }
+ if (pt.baseType == Type.tFixedArray) {
+ var visiting : table
+ let inner = pt.firstType == null ? CType(ok = false) : field_c_type(pt.firstType, names, visiting)
+ return inner.ok ? CType(ok = true, spelling = inner.spelling, by_pointer = true) : CType(ok = false)
+ }
+ let val = value_c_type(pt, names)
+ if (val.ok || !allow_opaque) {
+ return val
+ }
+ return opaque_c_of(pt, names)
+}
+
+
+struct public CParam {
+ name : string
+ c_type : string
+ by_pointer : bool
+ is_const : bool
+ pointer_in_impl : bool
+}
+
+
+struct public CResult {
+ c_type : string
+ via_out : bool
+ cmres : bool
+}
+
+
+struct public CExport {
+ @do_not_delete fn : Function?
+ c_name : string
+ das_signature : string
+ params : array
+ result : CResult
+}
+
+
+struct CReject {
+ ok : bool
+ what : string
+ why : string
+}
+
+
+def private param_name_of(raw : string; index : int) : string {
+ let n = c_ident(raw)
+ if (n |> empty()) {
+ return "a{index}"
+ }
+ return n == "ctx" || n == "out" ? "{n}_" : n
+}
+
+
+def private describe_params(var fn : Function?; names : CNames; var out : array) : CReject {
+ for (arg, i in fn.arguments, count()) {
+ let ct = c_type_of(arg._type, names, true)
+ if (!ct.ok || arg._type.isVoid) {
+ return CReject(what = "parameter `{arg.name}`", why = arg._type.describe())
+ }
+ let by_ptr = ct.by_pointer || arg._type.flags.ref
+ out |> emplace(CParam(name = param_name_of(string(arg.name), i),
+ c_type = ct.spelling,
+ by_pointer = by_ptr,
+ is_const = by_ptr && arg._type.flags.constant,
+ pointer_in_impl = arg._type.isRef))
+ }
+ return CReject(ok = true)
+}
+
+
+def private returns_cmres(var fn : Function?) : bool {
+ return fn.flags.copyOnReturn || fn.flags.moveOnReturn
+}
+
+
+def public describe_c_signature(var fn : Function?; names : CNames; c_name : string) : tuple {
+ var params : array
+ let bad = describe_params(fn, names, params)
+ if (!bad.ok) {
+ return (exp = CExport(), reject = bad)
+ }
+ let rt = c_type_of(fn.result, names)
+ if (!rt.ok || peel(fn.result).baseType == Type.tFixedArray) {
+ return (exp = CExport(), reject = CReject(what = "result", why = fn.result.describe()))
+ }
+ let res = CResult(c_type = rt.spelling, via_out = rt.by_pointer, cmres = returns_cmres(fn))
+ return (exp = CExport(fn = fn, c_name = c_name, das_signature = das_signature_of(fn),
+ params <- params, result = res), reject = CReject(ok = true))
+}
+
+
+def private das_signature_of(var fn : Function?) : string {
+ let args = [for (a in fn.arguments); "{a.name} : {a._type.describe() |> replace(" const", "")}"]
+ return "def {fn.name}({args |> join("; ")}) : {fn.result.describe() |> replace(" const", "")}"
+}
+
+
+struct CEnum {
+ @do_not_delete en : Enumeration?
+ base : string
+}
+
+
+struct CHandle {
+ c_name : string
+ cpp_name : string
+ wrap : string
+ lanes : int
+ bytes : int
+ align : int
+ by_pointer : bool
+}
+struct CTypeSet {
+ enums : array
+ vectors : array
+ handles : array
+ @do_not_delete structs : array
+ @do_not_delete opaque : array
+ handle_names : array
+ seen : table
+}
+
+
+def private collect_type(var t : TypeDeclPtr; names : CNames; var set : CTypeSet) {
+ if (t == null) {
+ return
+ }
+ var pt = peel(t)
+ if (pt.isPointer) {
+ var pointee = pt.firstType == null ? TypeDeclPtr() : peel(pt.firstType)
+ if (pointee != null && pointee.isStructure) {
+ var visiting : table
+ if (struct_c_ok(pointee.structType, names, visiting)) {
+ collect_struct(pointee.structType, names, set)
+ } else {
+ collect_opaque_struct(pointee.structType, set)
+ }
+ } else {
+ collect_type(pt.firstType, names, set)
+ }
+ return
+ }
+ if (pt.baseType == Type.tFixedArray) {
+ collect_type(pt.firstType, names, set)
+ return
+ }
+ if (pt.isEnumT && pt.enumType != null && !pt.enumType.external) {
+ let key = "e:{pt.enumType._module.name}::{pt.enumType.name}"
+ if (!(set.seen |> key_exists(key))) {
+ set.seen |> insert(key)
+ set.enums |> push(CEnum(en = pt.enumType,
+ base = c_int_name(!is_unsigned_type(pt.enumType.baseType), pt.sizeOf)))
+ }
+ return
+ }
+ let v = vector_of(pt)
+ if (v.ok) {
+ let key = "v:{v.name}"
+ if (!(set.seen |> key_exists(key))) {
+ set.seen |> insert(key)
+ set.vectors |> push(v)
+ }
+ return
+ }
+ if (pt.baseType == Type.tHandle) {
+ collect_handle(pt, names, set)
+ return
+ }
+ if (pt.isStructure) {
+ collect_struct(pt.structType, names, set)
+ return
+ }
+ collect_opaque_tag(pt, names, set)
+}
+
+
+def private collect_opaque_tag(var t : TypeDeclPtr; names : CNames; var set : CTypeSet) {
+ let op = opaque_c_of(t, names)
+ if (op.ok && !(set.seen |> key_exists("o:{op.spelling}"))) {
+ set.seen |> insert("o:{op.spelling}")
+ set.handle_names |> push(op.spelling)
+ }
+}
+
+
+def private collect_handle(var t : TypeDeclPtr; names : CNames; var set : CTypeSet) {
+ var wrap <- handle_wrap_type(t)
+ let inner = wrap == null ? CType(ok = false) : value_c_type(wrap, names)
+ if (!inner.ok) {
+ collect_opaque_tag(t, names, set)
+ return
+ }
+ let key = "h:{t.annotation._module.name}::{t.annotation.name}"
+ if (set.seen |> key_exists(key)) {
+ return
+ }
+ set.seen |> insert(key)
+ let v = vector_of(wrap)
+ set.handles |> push(CHandle(
+ c_name = c_type_name(names, t.annotation._module, t.annotation.name),
+ cpp_name = string(t.annotation.cppName |> empty() ? t.annotation.name : t.annotation.cppName),
+ wrap = v.ok ? v.elem : inner.spelling, lanes = v.ok ? v.lanes : 0,
+ bytes = t.sizeOf, align = t.alignOf, by_pointer = inner.by_pointer))
+}
+
+
+def private collect_opaque_struct(var st : Structure?; var set : CTypeSet) {
+ if (st == null) {
+ return
+ }
+ let key = "o:{st._module.name}::{st.name}"
+ if (set.seen |> key_exists(key)) {
+ return
+ }
+ set.seen |> insert(key)
+ set.opaque |> push(st)
+}
+
+
+def private collect_struct(var st : Structure?; names : CNames; var set : CTypeSet) {
+ if (st == null) {
+ return
+ }
+ let key = "s:{st._module.name}::{st.name}"
+ if (set.seen |> key_exists(key)) {
+ return
+ }
+ set.seen |> insert(key)
+ for (fld in st.fields) {
+ collect_type(fld._type, names, set)
+ }
+ set.structs |> push(st)
+}
+
+
+def public collect_c_types(var exports : array; names : CNames) : CTypeSet {
+ var set : CTypeSet
+ for (e in exports) {
+ for (arg in e.fn.arguments) {
+ collect_type(arg._type, names, set)
+ }
+ collect_type(e.fn.result, names, set)
+ }
+ return <- set
+}
+
+
+def private enum_value_of(var en : Enumeration?; name : das_string; base : string) : string {
+ let v = en |> find_enum_value(string(name))
+ let is_unsigned = base |> starts_with("u")
+ let digits = is_unsigned ? "{uint64(v):d}" : "{v:d}"
+ if (base == "int64_t" || base == "uint64_t") {
+ return "{is_unsigned ? "UINT64_C" : "INT64_C"}({digits})"
+ }
+ return is_unsigned ? "{digits}u" : digits
+}
+
+
+def private write_enum(var writer : StringBuilderWriter; var e : CEnum; names : CNames) {
+ var en = e.en
+ let cname = c_type_name(names, en._module, en.name)
+ write(writer, "\n// das: enum {en.name}\ntypedef {e.base} {cname};\n")
+ write(writer, "enum \{\n")
+ let entries = [for (ee in en.list); " {cname}_{c_ident_part(string(ee.name))} = {enum_value_of(en, ee.name, e.base)}"]
+ write(writer, "{entries |> join(",\n")}\n\};\n")
+}
+
+
+def private write_handle(var writer : StringBuilderWriter; h : CHandle; names : CNames) {
+ let guard = names.prefix |> to_upper()
+ let body = h.lanes == 0 ? h.wrap : "struct \{ {guard}_ALIGNAS({h.align}) {h.wrap} lanes[{h.lanes}]; \}"
+ write(writer, "\n// das: {h.cpp_name} - a bound value type, carried as its ABI wrap type. Pass it\n")
+ write(writer, "// back as you received it; the lanes are the ABI's, not the type's own fields.\ntypedef {body} {h.c_name};\n")
+ write(writer, "{guard}_STATIC_ASSERT(sizeof({h.c_name}) == {h.bytes}, \"{h.c_name}: size differs from the daslang layout\");\n")
+ write(writer, "{guard}_STATIC_ASSERT({guard}_ALIGNOF({h.c_name}) == {h.align}, \"{h.c_name}: alignment differs from the daslang layout\");\n")
+}
+
+
+def private write_vector(var writer : StringBuilderWriter; v : CVector; names : CNames) {
+ let cname = "{names.prefix}_{v.name}"
+ let fields = vector_field_names(v) |> join(", ")
+ write(writer, "\n// das: {v.name}\ntypedef struct \{ {v.elem} {fields}; \} {cname};\n")
+ write(writer, "{names.prefix |> to_upper()}_STATIC_ASSERT(sizeof({cname}) == {v.bytes}, \"{cname}: size differs from the daslang layout\");\n")
+}
+
+
+def private field_decl(var fld : FieldDeclaration; names : CNames) : string {
+ var visiting : table
+ var ft = peel(fld._type)
+ var dims : array
+ while (ft.baseType == Type.tFixedArray && ft.firstType != null) {
+ dims |> push("[{ft.fixedDim}]")
+ ft = peel(ft.firstType)
+ }
+ let ct = field_c_type(fld._type, names, visiting)
+ return " {ct.spelling} {c_ident(string(fld.name))}{dims |> join("")};"
+}
+
+
+def private write_struct(var writer : StringBuilderWriter; var st : Structure?; names : CNames) {
+ let cname = c_type_name(names, st._module, st.name)
+ let guard = names.prefix |> to_upper()
+ write(writer, "\n// das: struct {st.name}\nstruct {cname} \{\n")
+ for (fld in st.fields) {
+ write(writer, "{field_decl(fld, names)}\n")
+ }
+ write(writer, "\};\n")
+ write(writer, "{guard}_STATIC_ASSERT(sizeof({cname}) == {st.sizeOf}, \"{cname}: size differs from the daslang layout\");\n")
+ for (fld in st.fields) {
+ write(writer, "{guard}_STATIC_ASSERT(offsetof({cname}, {c_ident(string(fld.name))}) == {fld.offset}, \"{cname}.{fld.name}: offset differs from the daslang layout\");\n")
+ }
+}
+
+
+def private param_decl(p : CParam) : string {
+ if (!p.by_pointer) {
+ return "{p.c_type} {p.name}"
+ }
+ return "{p.is_const ? "const " : ""}{p.c_type} * {p.name}"
+}
+
+
+def public c_declaration(e : CExport; names : CNames) : string {
+ var args = ["{names.prefix}_ctx * ctx"]
+ args |> reserve(length(e.params) + 2)
+ for (p in e.params) {
+ args |> push(param_decl(p))
+ }
+ if (e.result.via_out) {
+ args |> push("{e.result.c_type} * out")
+ }
+ let ret = e.result.via_out ? "void" : e.result.c_type
+ let arg_list = args |> join(", ")
+ return "{names.prefix |> to_upper()}_API {ret} {e.c_name}({arg_list})"
+}
+
+
+def private write_prologue(var writer : StringBuilderWriter; names : CNames; generated_by : string) {
+ let guard = names.prefix |> to_upper()
+ write(writer, "// Code generated by `{generated_by}`. DO NOT EDIT.\n")
+ write(writer, "// Layouts are asserted for the daslang that generated this header; regenerate it for another target.\n")
+ write(writer, "#pragma once\n\n#include \n#include \n#include \n\n")
+ write(writer, "#if defined(__cplusplus)\n#define {guard}_STATIC_ASSERT(cond, msg) static_assert(cond, msg)\n")
+ write(writer, "#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L\n")
+ write(writer, "#define {guard}_STATIC_ASSERT(cond, msg) _Static_assert(cond, msg)\n")
+ write(writer, "#else\n#define {guard}_STATIC_ASSERT(cond, msg)\n#endif\n\n")
+ write(writer, "// Building the generated .cpp into a Windows DLL exports nothing unless the entry points say\n")
+ write(writer, "// so: define {guard}_SHARED on both sides, and {guard}_BUILD on the library's own\n")
+ write(writer, "// translation unit. Everywhere else, and for a static link, both expand to nothing.\n")
+ write(writer, "#if defined(_WIN32) && defined({guard}_SHARED)\n")
+ write(writer, "#if defined({guard}_BUILD)\n#define {guard}_API __declspec(dllexport)\n")
+ write(writer, "#else\n#define {guard}_API __declspec(dllimport)\n#endif\n")
+ write(writer, "#else\n#define {guard}_API\n#endif\n\n")
+ write(writer, "#if defined(__cplusplus)\n#define {guard}_ALIGNAS(n) alignas(n)\n")
+ write(writer, "#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L\n")
+ write(writer, "#define {guard}_ALIGNAS(n) _Alignas(n)\n")
+ write(writer, "#elif defined(_MSC_VER)\n#define {guard}_ALIGNAS(n) __declspec(align(n))\n")
+ write(writer, "#else\n#define {guard}_ALIGNAS(n) __attribute__((aligned(n)))\n#endif\n")
+ write(writer, "#if defined(__cplusplus)\n#define {guard}_ALIGNOF(t) alignof(t)\n")
+ write(writer, "#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L\n")
+ write(writer, "#define {guard}_ALIGNOF(t) _Alignof(t)\n")
+ write(writer, "#else\n#define {guard}_ALIGNOF(t) __alignof__(t)\n#endif\n\n")
+ write(writer, "#ifdef __cplusplus\nextern \"C\" \{\n#endif\n")
+}
+
+
+def private write_fixed_api(var writer : StringBuilderWriter; names : CNames) {
+ let p = names.prefix
+ let guard = p |> to_upper()
+ write(writer, "\n// One library instance and everything it owns: globals, heap, string heap. Drive one\n")
+ write(writer, "// instance from one thread at a time, or make one per thread.\n")
+ write(writer, "typedef struct {p}_ctx {p}_ctx;\n")
+ write(writer, "\n// Creates an instance, then runs the script's global initializers and its [init] functions.\n")
+ write(writer, "// Callable from any thread, and from a process that already carries a daslang runtime -\n")
+ write(writer, "// another such library, or a host that registered the daslang modules itself. Returns NULL\n")
+ write(writer, "// when the initializers raised; {p}_last_error(NULL) reports why.\n")
+ write(writer, "{guard}_API {p}_ctx * {p}_create(void);\n")
+ write(writer, "\n// Runs the script's [finalize] functions, then frees the instance and everything it\n")
+ write(writer, "// allocated - every string and every pointer it returned included.\n")
+ write(writer, "{guard}_API void {p}_destroy({p}_ctx * ctx);\n")
+ write(writer, "\n// NULL when the last call on `ctx` completed normally; otherwise that call's exception text,\n")
+ write(writer, "// owned by the instance and valid until the next call on it. Pass NULL to read a failed\n")
+ write(writer, "// {p}_create. On a failed call the returned value is 0/false/NULL and `out` is left untouched.\n")
+ write(writer, "{guard}_API const char * {p}_last_error({p}_ctx * ctx);\n")
+ write(writer, "\n// Drains the daslang runtime for the whole process. Optional, and final: no instance of any\n")
+ write(writer, "// daslang library may be created or called afterwards.\n")
+ write(writer, "{guard}_API void {p}_shutdown_runtime(void);\n")
+ write(writer, "\n// A `const char *` an exported function returns points into the instance's string heap, and a\n")
+ write(writer, "// returned `T *` into its object heap: copy what you need past the next call on that instance,\n")
+ write(writer, "// because a later call may collect. The empty daslang string comes back NULL. A pointer you\n")
+ write(writer, "// pass in, `const char *` included, is borrowed for the duration of the call.\n")
+}
+
+
+def private write_c_section(var writer : StringBuilderWriter; var exports : array; names : CNames;
+ generated_by, link_note : string) {
+ var types <- collect_c_types(exports, names)
+ write_prologue(writer, names, generated_by)
+ write_fixed_api(writer, names)
+ for (en in types.enums) {
+ write_enum(writer, en, names)
+ }
+ for (vt in types.vectors) {
+ write_vector(writer, vt, names)
+ }
+ for (h in types.handles) {
+ write_handle(writer, h, names)
+ }
+ var defined : table
+ for (st in types.structs) {
+ defined |> insert(c_type_name(names, st._module, st.name))
+ }
+ if (!(types.handle_names |> empty())) {
+ write(writer, "\n// values only daslang can build: hold the pointer, hand it back, never read through it\n")
+ for (h in types.handle_names) {
+ write(writer, "typedef struct {h} {h};\n")
+ }
+ }
+ if (!(types.structs |> empty()) || !(types.opaque |> empty())) {
+ write(writer, "\n")
+ for (st in types.structs) {
+ let cname = c_type_name(names, st._module, st.name)
+ write(writer, "typedef struct {cname} {cname};\n")
+ }
+ for (st in types.opaque) {
+ let cname = c_type_name(names, st._module, st.name)
+ if (!(defined |> key_exists(cname))) {
+ write(writer, "typedef struct {cname} {cname};\n")
+ }
+ }
+ }
+ for (st in types.structs) {
+ write_struct(writer, st, names)
+ }
+ write(writer, "\n")
+ for (e in exports) {
+ write(writer, "\n// {e.das_signature}\n{c_declaration(e, names)};\n")
+ }
+ if (!(link_note |> empty())) {
+ write(writer, "\n// Linking:\n")
+ for (line in link_note |> split("\n")) {
+ write(writer, "// {line}\n")
+ }
+ }
+ write(writer, "\n#ifdef __cplusplus\n\} // extern \"C\"\n#endif\n")
+}
+
+
+def public build_c_header(var exports : array; names : CNames; input_path, output_path, link_note : string) : string {
+ return build_string() $(var writer) {
+ write_c_section(writer, exports, names, "daslang -lib {input_path} -output {output_path}", link_note)
+ }
+}
+
+
+def public emit_c_header(var exports : array; names : CNames; input_path, output_path, link_note : string) : bool {
+ let text = build_c_header(exports, names, input_path, output_path, link_note)
+ let path = "{output_path}.h"
+ var ok = false
+ fopen(path, "wb") $(f) {
+ if (f != null) {
+ f |> fwrite(text)
+ ok = true
+ }
+ }
+ if (!ok) {
+ to_log(LOG_ERROR, "daslang -lib: can't write the C header {path}\n")
+ }
+ return ok
+}
+
+
+struct public CRejection {
+ fn_name : string
+ message : string
+}
+
+
+def public is_c_export_candidate(var fn : Function?; prog : Program?; export_all : bool) : bool {
+ if (fn._module != prog.getThisModule || !fn.flags.exports
+ || fn.flags.builtIn || fn.flags.generated || fn.moreFlags.isTemplate || fn.fromGeneric != null
+ || fn.flags.init || fn.flags.shutdown || fn.flags.macroInit || fn.moreFlags.macroFunction) {
+ return false
+ }
+ return export_all || has_export_c_annotation(fn)
+}
+
+
+def private reserved_c_names(names : CNames) : array {
+ let p = names.prefix
+ return ["{p}_create", "{p}_destroy", "{p}_last_error", "{p}_shutdown_runtime", "{p}_ctx"]
+}
+
+
+def public collect_c_exports(prog : Program?; names : CNames; export_all : bool) : tuple; errors : array> {
+ var accepted : array
+ var errors : array
+ var claimed <- {for (r in reserved_c_names(names)); r => "the fixed library API"}
+ var this_mod = prog.getThisModule
+ this_mod |> for_each_function("") $(var fn) {
+ if (!is_c_export_candidate(fn, prog, export_all)) {
+ return
+ }
+ let is_explicit = has_export_c_annotation(fn)
+ let c_name = c_export_symbol(names.prefix, fn, export_c_name_override(fn))
+ var why = ""
+ let owner = claimed?[c_name] ?? ""
+ var sig <- describe_c_signature(fn, names, c_name)
+ if (!(owner |> empty())) {
+ why = "C symbol `{c_name}` is already taken by {owner}; C has no overloading - give one of them [export_c(name = \"...\")]"
+ } elif (!sig.reject.ok) {
+ why = ("{sig.reject.what} has type {sig.reject.why}, which has no C representation "
+ + "(allowed: bool, int8..uint64, int, uint, float, double, string, pointers, enums, "
+ + "POD structs, bound value types, float2..uint4, range/urange/range64/urange64, "
+ + "fixed_array arguments)")
+ }
+ if (!(why |> empty())) {
+ if (is_explicit) {
+ errors |> emplace(CRejection(fn_name = string(fn.name),
+ message = "{describe(fn.at)}: [export_c] {fn.name}: {why}"))
+ } else {
+ to_log(LOG_WARNING, "{describe(fn.at)}: skipping {fn.name} in the C API: {why}\n")
+ }
+ return
+ }
+ claimed[c_name] = "{fn.name} at {describe(fn.at)}"
+ accepted |> emplace(sig.exp)
+ }
+ return (exports <- accepted, errors <- errors)
+}
+
+
+struct public CppApi {
+ class_name : string
+ namespace_name : string
+ includes : string
+ type_defs : string
+ class_decl : string
+ registers_modules : bool
+}
+
+
+def private write_cpp_section(var writer : StringBuilderWriter; cpp : CppApi) {
+ write(writer, "\n#ifdef __cplusplus\n\n")
+ write(writer, cpp.includes)
+ write(writer, "namespace das \{\n")
+ write(writer, cpp.type_defs)
+ write(writer, "namespace {cpp.namespace_name} \{\n\n")
+ let bases = cpp.registers_modules ? "public StandaloneModuleScope, public Context" : "public Context"
+ write(writer, "class {cpp.class_name} : {bases} \{\npublic:\n {cpp.class_name}();\n")
+ write(writer, cpp.class_decl)
+ write(writer, "\};\n\n")
+ write(writer, "\} // namespace {cpp.namespace_name}\n\} // namespace das\n\n#endif // __cplusplus\n")
+}
+
+
+def public build_standalone_header(var exports : array; names : CNames; cpp : CppApi;
+ generated_by, link_note : string) : string {
+ return build_string() $(var writer) {
+ write_c_section(writer, exports, names, generated_by, link_note)
+ write_cpp_section(writer, cpp)
+ }
+}
+
+
+struct public DasBindings {
+ linux_path : string
+ macos_path : string
+ windows_path : string
+}
+
+
+def private das_scalar_of(var t : TypeDeclPtr) : string {
+ if (t.isBool) {
+ return "bool"
+ }
+ if (t.isFloatOrDouble) {
+ return t.sizeOf == 4 ? "float" : "double"
+ }
+ if (t.isInteger || t.isBitfield) {
+ let bits = t.sizeOf * 8
+ let sign = t.isBitfield || !t.isSignedInteger ? "u" : ""
+ return bits == 32 ? "{sign}int" : "{sign}int{bits}"
+ }
+ return ""
+}
+
+
+def private das_struct_ok(var st : Structure?; names : CNames) : bool {
+ if (st == null) {
+ return false
+ }
+ for (fld in st.fields) {
+ if (das_field_of(fld, names) |> empty()) {
+ return false
+ }
+ }
+ return true
+}
+
+
+def private das_value_type(var raw : TypeDeclPtr; names : CNames) : string {
+ var t = peel(raw)
+ if (t.isString) {
+ return "string"
+ }
+ if (t.isEnumT) {
+ return t.enumType == null || t.enumType.external ? "" : "int"
+ }
+ if (t.baseType == Type.tHandle) {
+ return ""
+ }
+ if (t.isPointer) {
+ return "void?"
+ }
+ let v = vector_of(t)
+ if (v.ok) {
+ return v.name
+ }
+ if (t.isStructure) {
+ if (!das_struct_ok(t.structType, names)) {
+ return ""
+ }
+ return c_type_name(names, t.structType._module, t.structType.name)
+ }
+ return das_scalar_of(t)
+}
+
+
+def private das_field_of(var fld : FieldDeclaration; names : CNames) : string {
+ var t = peel(fld._type)
+ var dims : array
+ while (t.baseType == Type.tFixedArray && t.firstType != null) {
+ dims |> push("[{t.fixedDim}]")
+ t = peel(t.firstType)
+ }
+ if (length(dims) > 1) {
+ return ""
+ }
+ let base = das_value_type(t, names)
+ let suffix = dims |> join("")
+ return base |> empty() ? "" : "{base}{suffix}"
+}
+
+
+def private das_param_of(p : CParam; var raw : TypeDeclPtr; names : CNames) : string {
+ var t = peel(raw)
+ if (t.baseType == Type.tFixedArray && t.firstType != null) {
+ let elem = das_value_type(t.firstType, names)
+ return elem |> empty() ? "" : "{elem}?#"
+ }
+ let base = das_value_type(t, names)
+ if (base |> empty()) {
+ return ""
+ }
+ return p.by_pointer ? "{base}?#" : base
+}
+
+
+def private dasbind_can_wrap(float_at : array) : bool {
+ if (length(float_at) <= 6) {
+ return true
+ }
+ for (i in range(6, length(float_at))) {
+ if (float_at[i]) {
+ return false
+ }
+ }
+ return true
+}
+
+
+def private das_extern_signature(var e : CExport; names : CNames) : tuple {
+ var fn = e.fn
+ var args = ["ctx : void?"]
+ var float_at = [false]
+ args |> reserve(length(e.params) + 2)
+ for (pp, variable in e.params, fn.arguments) {
+ let spell = das_param_of(pp, variable._type, names)
+ if (spell |> empty()) {
+ let what = describe(variable._type)
+ return (sig = "", why = "argument `{pp.name}` is {what}")
+ }
+ args |> push("{pp.name} : {spell}")
+ float_at |> push(spell == "float" || spell == "double")
+ }
+ var result = "void"
+ if (e.result.via_out) {
+ let out_type = das_value_type(fn.result, names)
+ if (out_type |> empty()) {
+ let what = describe(fn.result)
+ return (sig = "", why = "result is {what}")
+ }
+ args |> push("out : {out_type}?#")
+ float_at |> push(false)
+ } elif (!fn.result.isVoid) {
+ result = das_value_type(fn.result, names)
+ if (result |> empty()) {
+ let what = describe(fn.result)
+ return (sig = "", why = "result is {what}")
+ }
+ }
+ if (!dasbind_can_wrap(float_at)) {
+ return (sig = "", why = "dasbind has no SysV wrapper for a floating-point argument past the sixth")
+ }
+ let arg_list = args |> join("; ")
+ return (sig = "{e.c_name}({arg_list}) : {result}", why = "")
+}
+
+
+def private write_extern(var writer : StringBuilderWriter; b : DasBindings; sym, sig : string) {
+ write(writer, "[extern(cdecl, late, name=\"{sym}\",\n")
+ write(writer, " linux_library=\"{b.linux_path}\",\n")
+ write(writer, " macos_library=\"{b.macos_path}\",\n")
+ write(writer, " windows_library=\"{b.windows_path}\")]\n")
+ write(writer, "def {sig} \{\}\n\n")
+}
+
+
+def public build_das_bindings(var exports : array; names : CNames; b : DasBindings;
+ generated_by : string) : string {
+ var types <- collect_c_types(exports, names)
+ let p = names.prefix
+ return build_string() $(var writer) {
+ write(writer, "options gen2\n\n// Code generated by `{generated_by}`. DO NOT EDIT.\n\n")
+ write(writer, "require dasbind public\n\n")
+ for (st in types.structs) {
+ if (!das_struct_ok(st, names)) {
+ continue
+ }
+ write(writer, "struct {c_type_name(names, st._module, st.name)} \{\n")
+ for (fld in st.fields) {
+ write(writer, " {c_ident(string(fld.name))} : {das_field_of(fld, names)}\n")
+ }
+ write(writer, "\}\n\n")
+ }
+ write_extern(writer, b, "{p}_create", "{p}_create() : void?")
+ write_extern(writer, b, "{p}_destroy", "{p}_destroy(ctx : void?) : void")
+ write_extern(writer, b, "{p}_last_error", "{p}_last_error(ctx : void?) : string")
+ write_extern(writer, b, "{p}_shutdown_runtime", "{p}_shutdown_runtime() : void")
+ for (e in exports) {
+ let spelled = das_extern_signature(e, names)
+ if (spelled.sig |> empty()) {
+ write(writer, "// {e.c_name} is declared in the C header only: {spelled.why}\n\n")
+ continue
+ }
+ write(writer, "// das: {e.das_signature}\n")
+ write_extern(writer, b, e.c_name, spelled.sig)
+ }
+ }
+}
+
+
+def public emit_das_bindings(var exports : array; names : CNames; b : DasBindings;
+ generated_by, path : string) : bool {
+ let text = build_das_bindings(exports, names, b, generated_by)
+ var ok = false
+ fopen(path, "wb") $(f) {
+ if (f != null) {
+ f |> fwrite(text)
+ ok = true
+ }
+ }
+ if (!ok) {
+ to_log(LOG_ERROR, "c_api_header: can't write the daslang bindings {path}\n")
+ }
+ return ok
+}
diff --git a/daslib/export_c.das b/daslib/export_c.das
new file mode 100644
index 0000000000..65d68a7726
--- /dev/null
+++ b/daslib/export_c.das
@@ -0,0 +1,49 @@
+options gen2
+options indenting = 4
+
+module export_c shared private !inscope
+
+require daslib/ast_boost
+require strings
+
+
+def private is_c_identifier(name : string) : bool {
+ if (name |> empty()) {
+ return false
+ }
+ var first = true
+ for (ch in name) {
+ let alpha = is_alpha(ch) || ch == '_'
+ if (!(alpha || (is_number(ch) && !first))) {
+ return false
+ }
+ first = false
+ }
+ return true
+}
+
+
+[function_macro(name = "export_c")]
+class private ExportCAnnotation : AstFunctionAnnotation {
+ def override apply(var func : FunctionPtr; var group : ModuleGroup;
+ args : AnnotationArgumentList; var errors : das_string) : bool {
+ for (arg in func.arguments) {
+ if (arg._type.baseType == Type.autoinfer && arg.init == null) {
+ errors := "[export_c] can't export generic function `{func.name}`: it has auto or template arguments, and C needs one concrete signature"
+ return false
+ }
+ }
+ for (arg in args) {
+ if (arg.name != "name") {
+ errors := "[export_c] unknown argument `{arg.name}`; the only argument is name=\"\""
+ return false
+ }
+ if (arg.basicType != Type.tString || !is_c_identifier(string(arg.sValue))) {
+ errors := "[export_c] name must be a valid C identifier ([A-Za-z_][A-Za-z0-9_]*), got `{arg.sValue}`"
+ return false
+ }
+ }
+ func.flags.exports = true
+ return true
+ }
+}
diff --git a/doc/source/reference/embedding/advanced.rst b/doc/source/reference/embedding/advanced.rst
index 2058b2ced3..1da3adea18 100644
--- a/doc/source/reference/embedding/advanced.rst
+++ b/doc/source/reference/embedding/advanced.rst
@@ -4,6 +4,7 @@
.. index::
single: Embedding; AOT
single: Embedding; Advanced Topics
+ single: Embedding; C Libraries
single: Embedding; Class Adapters
single: Embedding; Coroutines
single: Embedding; Standalone Contexts
@@ -267,13 +268,84 @@ Pipeline:
#include "standalone_ctx_generated/script.das.h"
- das::standalone::Standalone ctx;
+ das::ctx_standalone::Standalone ctx;
ctx.test(); // direct call, no findFunction needed
+The same generated files also carry a C API: one ``extern "C"`` entry
+point per export, wrapping the C++ method beside it, plus an opaque
+handle and ``_create`` / ``_destroy`` / ``_last_error``. So one
+generated pair serves a C++ host and a C host at once, and the two
+surfaces are not identical — C++ reaches every export in native types,
+while C carries the subset C can express (the emitter names the ones it
+skips). The ``.das.cpp`` embeds the header's text rather than including
+it, so moving or editing your copy of the header cannot break the
+library's own translation unit.
+
See :ref:`tutorial_integration_cpp_standalone_contexts` for a complete
example.
+C libraries
+===========
+
+``daslang -lib`` compiles a daslang program to a native library with a C
+API, so a host that only *calls* one script needs no daslang headers, no
+``Module``, and no compiler. The LLVM backend emits the library; a
+standalone context (above) emits C++ source your build compiles instead.
+
+Pipeline:
+
+1. ``daslang -lib script.das -output build/script``
+2. This writes ``build/script.so`` (``.dylib`` / ``.dll``) and
+ ``build/script.h``. Add ``-- --jit-lib-static`` for a ``.a`` / ``.lib``
+ archive instead; the header records what a host then links.
+3. Include the header, link the library, call it.
+
+Which functions cross the boundary is your choice: ``[export_c]`` marks
+them one at a time, and ``-lib-export-all`` offers every public function
+of the entry module whose signature C can spell, naming the ones it skips.
+
+Every library gets the same four entry points, prefixed with the output
+name — one instance owns one daslang context, with its own globals and
+heap:
+
+.. code-block:: c
+
+ #include "script.h"
+
+ script_ctx * ctx = script_create();
+ if ( !ctx ) {
+ printf("%s\n", script_last_error(NULL));
+ return 1;
+ }
+ script_Vec3 v = { 1.0f, 2.0f, 3.0f };
+ script_Vec3 out;
+ script_scale(ctx, &v, 2.0f, &out); /* a struct result uses a trailing out pointer */
+ script_destroy(ctx);
+
+A daslang panic never unwinds into C: the call returns zero and leaves
+``out`` untouched, and ``script_last_error(ctx)`` reports the text until
+the next call on that instance clears it. A ``const char *`` a function
+returns lives in that instance's string heap, so copy it if you need it
+past the next call.
+
+The generated header asserts the layout of every structure it declares
+(``_Static_assert`` in C11, ``static_assert`` in C++), so a host built
+for a different target fails to compile rather than misreading memory.
+
+A daslang host needs no C: ``-- --jit-lib-bindings build/script_c.das``
+writes the daslang twin of the header beside it, one
+``[extern(cdecl, late, ...)]`` per entry point plus a das struct per
+structure that crosses, and the host ``require``\ s that file. The externs
+bind ``late``, so the bindings compile before the library exists and a
+build system can generate them as an ordinary output.
+
+Several such libraries coexist in one process, and so does a library inside a
+host that registered the daslang modules itself: whoever gets there first
+registers the runtime, and the rest bind to it. Several instances of one
+library are fine, on any thread.
+
+
Serialization
=============
diff --git a/doc/source/reference/embedding/c_api.rst b/doc/source/reference/embedding/c_api.rst
index 12bfc739ea..68e81e1e78 100644
--- a/doc/source/reference/embedding/c_api.rst
+++ b/doc/source/reference/embedding/c_api.rst
@@ -37,6 +37,12 @@ functionality. The C API covers the most common embedding scenarios
but does not expose every C++ feature (e.g. class adapters, custom
annotations).
+This API embeds the daslang *compiler*: the host loads sources, compiles
+them, and calls what it finds by name. A host that only needs to call
+one fixed script wants ``daslang -lib`` instead — it compiles that script
+to a native library with its own generated C header, and the host links
+no daslang API at all. See :ref:`embedding_advanced` (C libraries).
+
Linking
=======
diff --git a/doc/source/reference/language/annotations.rst b/doc/source/reference/language/annotations.rst
index 15ec5828ff..b06c15f9a3 100644
--- a/doc/source/reference/language/annotations.rst
+++ b/doc/source/reference/language/annotations.rst
@@ -65,6 +65,41 @@ Lifecycle
print("hello\n")
}
+``[export_c]``
+ An ``[export]`` that ``daslang -lib`` additionally surfaces as a C function in the header it
+ generates. ``-lib``, ``-jit`` and ``-exe`` carry the annotation themselves, so a library source
+ needs no ``require``; a plain interpreter or AOT compile of the same file needs
+ ``require daslib/export_c``. The function keeps working in every tier. Its signature
+ has to be one C can spell: scalars, ``string``, raw pointers, enumerations, plain structures,
+ the ``float2``..``uint4`` and ``range`` families, and ``fixed_array`` arguments; ``array``,
+ ``table``, ``tuple``, ``variant``, lambdas, blocks, iterators and bound C++ types are not, and
+ the build names the parameter it could not spell. A generic function cannot carry it.
+ ``name="..."`` renames the C symbol, which is how two overloads reach C at all:
+
+ .. das-doc: alt
+ .. code-block:: das
+
+ struct Vec3 {
+ x : float
+ y : float
+ z : float
+ }
+
+ [export_c]
+ def dot(a, b : Vec3) : float {
+ return a.x * b.x + a.y * b.y + a.z * b.z
+ }
+
+ [export_c(name = "scale_by")]
+ def scale(v : Vec3; k : float) : Vec3 {
+ return Vec3(x = v.x * k, y = v.y * k, z = v.z * k)
+ }
+
+ A scalar result comes back directly, so ``dot`` becomes
+ ``float p_dot(p_ctx *, const p_Vec3 *, const p_Vec3 *)``; a structure or vector result travels
+ through a trailing out pointer, so ``scale`` becomes
+ ``void p_scale_by(p_ctx *, const p_Vec3 *, float, p_Vec3 *)``.
+
``[init]``
Marks a function to run automatically during context initialization. The function must
take no arguments and return ``void``:
diff --git a/doc/source/reference/tutorials/integration_cpp_20_standalone_contexts.rst b/doc/source/reference/tutorials/integration_cpp_20_standalone_contexts.rst
index c6922674d6..2e2f38592e 100644
--- a/doc/source/reference/tutorials/integration_cpp_20_standalone_contexts.rst
+++ b/doc/source/reference/tutorials/integration_cpp_20_standalone_contexts.rst
@@ -113,7 +113,7 @@ no compilation, no simulation:
TextPrinter tout;
tout << "Creating standalone context...\n";
- auto ctx = standalone_context::Standalone();
+ auto ctx = ctx_standalone_context::Standalone();
tout << "Calling test():\n";
ctx.test();
@@ -126,7 +126,7 @@ Key points:
* **No** ``NEED_ALL_DEFAULT_MODULES`` or ``Module::Initialize`` —
the standalone context is entirely self-contained.
-* ``standalone_context::Standalone()`` — the constructor sets up all
+* ``ctx_standalone_context::Standalone()`` — the constructor sets up all
functions, globals, and type info from pre-generated AOT data.
* ``ctx.test()`` — a direct C++ method call, not ``findFunction``
followed by ``evalWithCatch``.
diff --git a/doc/source/stdlib/handmade/structure_annotation-rtti-CodeOfPolicies.rst b/doc/source/stdlib/handmade/structure_annotation-rtti-CodeOfPolicies.rst
index 084f1782e1..3a5b89501b 100644
--- a/doc/source/stdlib/handmade/structure_annotation-rtti-CodeOfPolicies.rst
+++ b/doc/source/stdlib/handmade/structure_annotation-rtti-CodeOfPolicies.rst
@@ -14,6 +14,7 @@ Whether we are in lint-check mode (standalone linters set this so modules can ad
Skip Program::lint() entirely (as if every module set ``options lint = false``).
Skip the Module::Initialize() assert in compileDaScript (for environments initialized later, e.g. dynamic-module discovery).
Export all functions and global variables.
+Treat every public, non-generic function of the entry module as [export] (daslang -lib -lib-export-all).
If not set, we recompile main module each time.
Keep context alive after main function.
Whether to use very safe context (delete of data is delayed, to avoid table[foo]=table[bar] lifetime bugs).
diff --git a/examples/c_api_library/CMakeLists.txt b/examples/c_api_library/CMakeLists.txt
new file mode 100644
index 0000000000..44b130ceed
--- /dev/null
+++ b/examples/c_api_library/CMakeLists.txt
@@ -0,0 +1,57 @@
+###########################################################
+# Three daslang libraries with a C ABI, bound back into daslang.
+#
+# Each .das becomes a shared library through `daslang -lib`, and the same run writes the
+# daslang `[extern]` bindings for it - the das twin of the generated C header. main.das
+# requires those, so the example hand-writes no declaration and parses no C header.
+#
+# The bindings sit beside their source because a das `require` resolves relative to the
+# requiring file; they are generated, and .gitignore covers them.
+###########################################################
+
+if(DAS_LLVM_DISABLED)
+ return()
+endif()
+
+# The libraries live beside the source, the one place main.das also rebuilds them into, and
+# the -output path stays relative to the tree root so the bindings carry a relative library
+# path rather than this machine's absolute one.
+set(C_API_LIB_OUT "examples/c_api_library/_out")
+
+function(das_c_api_library stem select_flag jit_flag)
+ set(_bind "${CMAKE_CURRENT_SOURCE_DIR}/${stem}_c.das")
+ # one -lib run writes both, and main.das rebuilds neither: declare the library too, or a
+ # deleted artifact leaves the example bound to nothing
+ set(_lib "${CMAKE_CURRENT_SOURCE_DIR}/_out/${stem}${CMAKE_SHARED_LIBRARY_SUFFIX}")
+ add_custom_command(
+ OUTPUT "${_bind}" "${_lib}"
+ COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_CURRENT_SOURCE_DIR}/_out"
+ COMMAND $ -lib "${CMAKE_CURRENT_SOURCE_DIR}/${stem}.das"
+ -output "${C_API_LIB_OUT}/${stem}" ${select_flag}
+ -- ${jit_flag} --jit-lib-bindings "${_bind}"
+ DEPENDS daslang
+ "${CMAKE_CURRENT_SOURCE_DIR}/${stem}.das"
+ "${PROJECT_SOURCE_DIR}/daslib/c_api_header.das"
+ "${PROJECT_SOURCE_DIR}/modules/dasLLVM/daslib/jit_standalone.das"
+ "${PROJECT_SOURCE_DIR}/modules/dasLLVM/daslib/llvm_jit_run.das"
+ WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}"
+ COMMENT "daslang -lib bindings: ${stem}"
+ VERBATIM
+ )
+ set(C_API_LIB_BINDINGS ${C_API_LIB_BINDINGS} "${_bind}" PARENT_SCOPE)
+endfunction()
+
+das_c_api_library(shapes "" "")
+das_c_api_library(units -lib-export-all "")
+das_c_api_library(greetings "" --jit-lib-export-marked)
+
+# ALL, not just a test-small dependency: CI runs `ctest -L small` without building that target
+add_custom_target(c_api_library_bindings ALL DEPENDS ${C_API_LIB_BINDINGS})
+set_target_properties(c_api_library_bindings PROPERTIES FOLDER "examples")
+
+# The libraries and their bindings both come from the commands above; main.das builds nothing,
+# so the example is only about binding three libraries at once and calling them.
+add_test(NAME c_api_library COMMAND $ examples/c_api_library/main.das
+ WORKING_DIRECTORY ${PROJECT_SOURCE_DIR})
+set_tests_properties(c_api_library PROPERTIES LABELS "small")
+add_dependencies(test-small c_api_library_bindings)
diff --git a/examples/c_api_library/greetings.das b/examples/c_api_library/greetings.das
new file mode 100644
index 0000000000..71243a32c6
--- /dev/null
+++ b/examples/c_api_library/greetings.das
@@ -0,0 +1,17 @@
+options gen2
+options indenting = 4
+
+
+[export]
+def greet(who : string) : string {
+ return "hola {who}"
+}
+
+[export]
+def bump(n : int) : int {
+ return n + 1
+}
+
+def internal_seed() : int {
+ return 41
+}
diff --git a/examples/c_api_library/main.das b/examples/c_api_library/main.das
new file mode 100644
index 0000000000..3a75702611
--- /dev/null
+++ b/examples/c_api_library/main.das
@@ -0,0 +1,43 @@
+options gen2
+options indenting = 4
+
+
+require daslib/safe_addr
+
+require shapes_c
+require units_c
+require greetings_c
+
+[export]
+def main() : int {
+ var sh = shapes_create()
+ var un = units_create()
+ var gr = greetings_create()
+ if (sh == null || un == null || gr == null) {
+ print("a library refused to create an instance: {shapes_last_error(null)}\n")
+ return 1
+ }
+ var a = shapes_Vec2(x = 3.0, y = 4.0)
+ var b = shapes_Vec2(x = 1.0, y = 2.0)
+ let dot = shapes_dot(sh, safe_addr(a), safe_addr(b))
+ let fahrenheit = units_celsius_to_fahrenheit(un, 100.0)
+ let greeting = greetings_greet(gr, "mundo")
+ print("shapes.dot((3,4),(1,2)) = {dot}\n")
+ print("units.celsius_to_fahrenheit(100) = {fahrenheit}\n")
+ print("greetings.greet(\"mundo\") = {greeting}\n")
+ greetings_destroy(gr)
+ units_destroy(un)
+ shapes_destroy(sh)
+ var bad = 0
+ if (dot != 11.0) {
+ bad++
+ }
+ if (fahrenheit != 212.0) {
+ bad++
+ }
+ if (greeting != "hola mundo") {
+ bad++
+ }
+ print(bad == 0 ? "three daslang libraries, one process - ok\n" : "{bad} of three answered wrong\n")
+ return bad
+}
diff --git a/examples/c_api_library/shapes.das b/examples/c_api_library/shapes.das
new file mode 100644
index 0000000000..1f2ac59a14
--- /dev/null
+++ b/examples/c_api_library/shapes.das
@@ -0,0 +1,24 @@
+options gen2
+options indenting = 4
+
+
+require daslib/export_c
+
+struct Vec2 {
+ x : float
+ y : float
+}
+
+[export_c]
+def dot(a, b : Vec2) : float {
+ return a.x * b.x + a.y * b.y
+}
+
+[export_c(name = "scaled")]
+def scale(v : Vec2; k : float) : Vec2 {
+ return Vec2(x = v.x * k, y = v.y * k)
+}
+
+def length_squared(v : Vec2) : float {
+ return dot(v, v)
+}
diff --git a/examples/c_api_library/units.das b/examples/c_api_library/units.das
new file mode 100644
index 0000000000..8acede2724
--- /dev/null
+++ b/examples/c_api_library/units.das
@@ -0,0 +1,17 @@
+options gen2
+options indenting = 4
+
+require math
+
+
+def celsius_to_fahrenheit(c : float) : float {
+ return c * 1.8 + 32.0
+}
+
+def clamp_int(v, lo, hi : int) : int {
+ return clamp(v, lo, hi)
+}
+
+def private rounding_bias() : float {
+ return 0.5
+}
diff --git a/examples/standalone/01_pure/main.cpp b/examples/standalone/01_pure/main.cpp
index e39e6e3e79..cdf1b5764b 100644
--- a/examples/standalone/01_pure/main.cpp
+++ b/examples/standalone/01_pure/main.cpp
@@ -37,18 +37,18 @@ int main () {
// the context exists means even a panic during construction is visible.
das_nano_set_print(&to_console);
- pure_math::Standalone ctx;
+ ctx_pure_math::Standalone ctx;
- pure_math::Vec3 a; a.x = 1.0f; a.y = 2.0f; a.z = 3.0f;
- pure_math::Vec3 b; b.x = 4.0f; b.y = 5.0f; b.z = 6.0f;
+ ctx_pure_math::Vec3 a; a.x = 1.0f; a.y = 2.0f; a.z = 3.0f;
+ ctx_pure_math::Vec3 b; b.x = 4.0f; b.y = 5.0f; b.z = 6.0f;
expect_float("dot(a,b)", ctx.dot(a, b), 32.0f);
- pure_math::Vec3 s = ctx.scale(a, 2.0f);
+ ctx_pure_math::Vec3 s = ctx.scale(a, 2.0f);
expect_float("scale(a,2).x", s.x, 2.0f);
expect_float("scale(a,2).z", s.z, 6.0f);
- expect_float("component(a,y)", ctx.component(a, pure_math::Axis::y), 2.0f);
+ expect_float("component(a,y)", ctx.component(a, ctx_pure_math::Axis::y), 2.0f);
expect_float("weighted_sum(a)", ctx.weighted_sum(a), 1.0f*0.25f + 2.0f*0.5f + 3.0f*0.25f);
expect_int("collatz_steps(27)", ctx.collatz_steps(27), 111);
diff --git a/examples/standalone/02_heap/main.cpp b/examples/standalone/02_heap/main.cpp
index 12821522a5..9b8cbba365 100644
--- a/examples/standalone/02_heap/main.cpp
+++ b/examples/standalone/02_heap/main.cpp
@@ -27,7 +27,7 @@ static void to_console ( const char * text ) {
int main () {
das_nano_set_print(&to_console);
- heap_demo::Standalone ctx;
+ ctx_heap_demo::Standalone ctx;
// 0..9 squared
expect_int("sum_range(10)", ctx.sum_range(10), 285);
diff --git a/examples/standalone/03_closures/main.cpp b/examples/standalone/03_closures/main.cpp
index 0245079941..ae1bb2434c 100644
--- a/examples/standalone/03_closures/main.cpp
+++ b/examples/standalone/03_closures/main.cpp
@@ -26,7 +26,7 @@ static void to_console ( const char * text ) {
int main () {
das_nano_set_print(&to_console);
- closures::Standalone ctx;
+ ctx_closures::Standalone ctx;
expect_int("apply_twice(10)", ctx.apply_twice(10), 16);
expect_int("call_through_pointer(21)", ctx.call_through_pointer(21), 42);
diff --git a/examples/standalone/04_c_binding/main.cpp b/examples/standalone/04_c_binding/main.cpp
index f4cf59e96e..c492124393 100644
--- a/examples/standalone/04_c_binding/main.cpp
+++ b/examples/standalone/04_c_binding/main.cpp
@@ -27,7 +27,7 @@ static void board_print ( const char * text ) {
int main () {
das_nano_set_print(&board_print);
- blinker::Standalone ctx;
+ ctx_blinker::Standalone ctx;
// One full sweep of the scanner: 1,2,4,8,4,2 then back to 1.
static const unsigned expected[] = { 1, 2, 4, 8, 4, 2, 1, 2 };
diff --git a/examples/standalone/05_compile_time_table/main.cpp b/examples/standalone/05_compile_time_table/main.cpp
index 265ca098aa..c0ea606c90 100644
--- a/examples/standalone/05_compile_time_table/main.cpp
+++ b/examples/standalone/05_compile_time_table/main.cpp
@@ -29,7 +29,7 @@ static int read_adc ( int sample ) {
int main () {
das_nano_set_print(&board_print);
- thermometer::Standalone ctx;
+ ctx_thermometer::Standalone ctx;
const int lo = ctx.adc_range_lo();
const int hi = ctx.adc_range_hi();
diff --git a/examples/standalone/06_full_runtime/main.cpp b/examples/standalone/06_full_runtime/main.cpp
index 8ba4d0d6d2..9f9cd3754a 100644
--- a/examples/standalone/06_full_runtime/main.cpp
+++ b/examples/standalone/06_full_runtime/main.cpp
@@ -24,7 +24,7 @@ int main ( int argc, char * argv[] ) {
return 7;
}
const char * url = argc > 1 ? argv[1] : "http://127.0.0.1:1/";
- service_probe::Standalone ctx;
+ ctx_service_probe::Standalone ctx;
const int status = ctx.http_status((char *)url);
printf("GET %s -> %d%s\n", url, status, status < 0 ? " (nobody answered)" : "");
const bool status_is_sane = status == -1 || (status >= 100 && status <= 599);
diff --git a/examples/standalone/CMakeLists.txt b/examples/standalone/CMakeLists.txt
index 256ad82b7d..dfcd8648f5 100644
--- a/examples/standalone/CMakeLists.txt
+++ b/examples/standalone/CMakeLists.txt
@@ -33,6 +33,7 @@ function(das_nano_example name dir das_file)
${ARGN}
"${PROJECT_SOURCE_DIR}/utils/aot/main.das"
"${PROJECT_SOURCE_DIR}/daslib/aot_standalone.das"
+ "${PROJECT_SOURCE_DIR}/daslib/c_api_header.das"
"${PROJECT_SOURCE_DIR}/daslib/aot_cpp.das"
WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}"
COMMENT "Standalone AOT: ${das_file}"
@@ -69,6 +70,7 @@ if(NOT DAS_HV_DISABLED)
"${CMAKE_CURRENT_SOURCE_DIR}/06_full_runtime/service_probe.das"
"${PROJECT_SOURCE_DIR}/utils/aot/main.das"
"${PROJECT_SOURCE_DIR}/daslib/aot_standalone.das"
+ "${PROJECT_SOURCE_DIR}/daslib/c_api_header.das"
"${PROJECT_SOURCE_DIR}/daslib/aot_cpp.das"
WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}"
COMMENT "Standalone AOT (full runtime): service_probe.das"
diff --git a/include/daScript/ast/dyn_modules.h b/include/daScript/ast/dyn_modules.h
index cef5660316..984da1aa51 100644
--- a/include/daScript/ast/dyn_modules.h
+++ b/include/daScript/ast/dyn_modules.h
@@ -45,6 +45,7 @@ DAS_API void replay_dynamic_module(const char * path, const char * cpp_class, in
DAS_API void defer_dynamic_module(const char * path, const char * cpp_class, int on_error, const char * das_name);
DAS_API bool load_deferred_dynamic_module(const char * das_name);
DAS_API size_t load_all_deferred_dynamic_modules(); // the count it attempted
+DAS_API string describe_pending_dynamic_modules(); // "" when nothing is pending
DAS_API bool is_dynamic_module_deferred(const char * das_name);
DAS_API bool pending_dynamic_module_artifact_present(); // a dlopen-failed row whose file exists: an import the deferred set holds
DAS_API string registered_dynamic_module_name(const char * path, const char * cpp_class); // empty while that row's module is not registered
diff --git a/include/daScript/simulate/code_of_policies.h b/include/daScript/simulate/code_of_policies.h
index 78bb940b67..2d27daf29f 100644
--- a/include/daScript/simulate/code_of_policies.h
+++ b/include/daScript/simulate/code_of_policies.h
@@ -41,6 +41,7 @@ namespace das {
bool no_lint = false; // skip Program::lint() entirely
bool no_init_check = false; // skip the Module::Initialize() assert, most of the time should be false (except maybe dynamic-module discovery)
bool export_all = false; // when user compiles, export all (public?) functions
+ bool export_public_functions = false;
bool serialize_main_module = true; // if false, then we recompile main module each time
bool keep_alive = false; // produce keep-alive noodes
/*option*/ bool very_safe_context = false; // context is very safe (does not release old memory from array or table grow, leaves it to GC)
diff --git a/install/CLAUDE.md b/install/CLAUDE.md
index 9d83ff71a1..16a77746cc 100644
--- a/install/CLAUDE.md
+++ b/install/CLAUDE.md
@@ -53,7 +53,7 @@ Task-specific instructions are in skill files under `skills/`. Read the relevant
| `skills/mcp_tools.md` | Full MCP tool table + live-API reference |
| `skills/das_formatting.md` | Creating or modifying any `.das` file |
| `skills/comment_style_hygiene.md` | Writing or reviewing comments, names, or local code shape in ANY language |
-| `skills/cpp_integration.md` | Embedding daslang in C++; binding types/functions/enums; shipping without the compiler (`libDaScriptNano`, or a standalone context on the full runtime) |
+| `skills/cpp_integration.md` | Embedding daslang in C++; binding types/functions/enums; shipping without the compiler (`libDaScriptNano`, or a standalone context on the full runtime); calling daslang from C (`daslang -lib`) |
| `skills/daslib_modules.md` | Using `daslib/` modules (linq, json, regex, etc.) |
| `skills/das_macros.md` | Compile-time macros, AST manipulation, qmacro/quote, gc_node patterns |
| `skills/daspkg.md` | Creating `.das_package` manifests, daspkg commands |
diff --git a/modules/dasLLVM/.das_module b/modules/dasLLVM/.das_module
index 413bcb6b09..b80c5f3a96 100644
--- a/modules/dasLLVM/.das_module
+++ b/modules/dasLLVM/.das_module
@@ -9,7 +9,7 @@ def initialize(project_path : string) {
"llvm_dsl",
"llvm_jit_intrin", "llvm_jit_common", "llvm_jit_lower", "llvm_dll_utils",
"llvm_jit_di",
- "llvm_exe", "llvm_macro", "llvm_jit_cli", "llvm_jit_run", "llvm_aot",
+ "llvm_exe", "jit_standalone", "llvm_macro", "llvm_jit_cli", "llvm_jit_run", "llvm_aot",
"llvm_jit_plan", // the emitter-free half of the driver: plan, DLL key, probe, install
"llvm_jit_link", // the JIT entry: a DLL cache hit binds here, a miss brings llvm_macro in
"llvm_env", // [EnvConfig] environment-knob registry (ENVIRONMENT.md generates from it)
diff --git a/modules/dasLLVM/ARCHITECTURE.md b/modules/dasLLVM/ARCHITECTURE.md
index 3ab092a30b..e0fd51e3e1 100644
--- a/modules/dasLLVM/ARCHITECTURE.md
+++ b/modules/dasLLVM/ARCHITECTURE.md
@@ -16,10 +16,11 @@ program: **hash** (the DLL key over the candidate set: per-function AOT hashes p
folds), **init** (engine + target machine), **declare** (LLVM function declarations for the jit
set), **probe** (open the cached DLL, compare per-function hashes), **irgen** (the das IR emitter
over every function), **optimize** (the LLVM pass pipeline at the requested level, plus the
-opt-in IR dump and the post-opt verify), **emit+link** (artifact production - for the DLL path
-`write_dll` in `llvm_jit_common.das`, itself **emit-obj**, machine-code emission, then **link**,
-the lld-link spawn), **install** (resolve externs, instrument sim nodes), and **finalize**
-(engine teardown / state install). On a hit irgen, optimize and emit+link read as zero.
+opt-in IR dump and the post-opt verify), **emit+link** (artifact production - `write_artifact` in
+`llvm_jit_common.das`, itself **emit-obj**, machine-code emission, then **link**; which artifact
+is the caller's choice, a JIT DLL or an exe or, for `-lib`, a shared library or static archive
+with the C header written beside it), **install** (resolve externs, instrument sim nodes), and
+**finalize** (engine teardown / state install). On a hit irgen, optimize and emit+link read as zero.
### 1.1 The timing contract
@@ -57,13 +58,17 @@ one of those files visible (re-pin `LLVM_JIT_EMITTER_HASH`), and the bump is owe
emitted code for identical inputs can differ - a comment, a nolint, or a same-value rewrite
inside an emitter file re-pins without a bump.
-`--jit-opt-level` (CLI, over `policies.jit_opt_level`, default 3) drives both the optimize
-pipeline and the DLL path's codegen-side target machine. `write_exe` and AOT-object emission
-(`emit_object_only`) deliberately stay at codegen level 3: shipped artifacts are not
-content-addressed, so a tier change there has no cache guard to catch it. At level 0 the
-injected tune-policy default becomes `fallback` (`jit_cli_opt_level()` in `llvm_tune.das`):
-tune winners are raced under O3 codegen, so an O0 run cannot represent them and must not block
-on the tuner to mint them.
+The per-artifact entry emitters are OUTSIDE that surface: `llvm_exe.das` (a standalone exe's
+`main`) and its `-lib` half (the C entry points and thunks) emit startup glue for artifacts
+nothing content-addresses, so neither is in `EMITTER_FILES` and neither owes a version bump. Both
+artifacts run in `LlvmJitMode.EXE`, which is what makes the exe startup shareable: the four
+`emit_standalone_*` helpers in `llvm_exe.das` are the shared halves, split so a library can put the
+process-global half behind a once guard and the per-context half behind its own catch boundary.
+
+### 1.3 A library's runtime
+
+`daslang -lib` emits an artifact that loads into a process it does not own; its runtime,
+environment and shutdown rules are `ARCHITECTURE_LIB.md` sec. 1.3.
## 2. Codegen identity - the DLL cache
diff --git a/modules/dasLLVM/ARCHITECTURE_LIB.md b/modules/dasLLVM/ARCHITECTURE_LIB.md
new file mode 100644
index 0000000000..4f7f801e50
--- /dev/null
+++ b/modules/dasLLVM/ARCHITECTURE_LIB.md
@@ -0,0 +1,67 @@
+# dasLLVM Architecture - the emitted library
+
+Companion to `ARCHITECTURE.md` in this folder; section numbers are unique across both files.
+
+### 1.3 A library's runtime is process-global, its environment is per-thread {#lib-runtime-scope}
+
+An exe owns its process: one thread runs `main`, registers the modules, and drains them on the way
+out. A library owns none of that, and three consequences shape its entry points.
+
+**A library's process state lives in the library, not in the runtime.** `inject_lib` emits three
+private globals - the init guard, the environment it bound, and the text of a create that returned
+no instance - and passes them to the runtime shims by pointer. So the shim layer holds no state of
+its own, two libraries in one process share none, and nothing needs a per-library table.
+
+**The environment is thread-local** (`daScriptEnvironment::bound` / `owned`,
+`include/daScript/ast/ast.h`), but the module registration behind it happens once. So
+`jit_lib_run_once(guard, env, init)` does both jobs: the first caller registers and records its
+environment, and every later caller - on any thread - is BOUND to that recording. Without the
+binding a second thread's `
_create` dereferences a null `getBound()->modules` inside
+`jit_init_extern_function`. The create-failure text sits in the library's slot for the same reason
+read from the other side: a C host reads it through `
_last_error(nullptr)`, which has no
+instance to ask, and a thread-local copy would answer null on every thread but the failing one.
+
+**One daslang runtime fits in a process, and every artifact in it shares that one.**
+`jit_register_Module_*` (`REGISTER_MODULE_IN_NAMESPACE`) carries no already-created guard and
+aborts on a second call, so every emitted registration goes through
+`jit_register_module_once(dasName, reg)`, which registers only when `Module::require(dasName)`
+finds nothing and otherwise hands back the module already there. EVERY site means both of them -
+the require walk and the extern collector's `ensure_module`, which reaches a module no `require`
+names (an `ast_core` extern called from a library aborted the host until it did). A library that finds a populated
+environment is a GUEST, and the guard records which it is - 1 for the library that registered the
+runtime, 2 for one that joined a populated one, decided in `jit_lib_run_once` before the init call.
+A guest skips `Module::Initialize` so `g_envTotal` stays balanced, and it never drains what it did
+not create. Both facts are read back off the guard: `jit_initialize_modules_done` and
+`jit_lib_shutdown` take its value, so ownership needs no second flag and a library arriving later
+cannot erase an earlier one's.
+
+**A guest reports a failed dynamic module; it does not abort.** `jit_finalize_dynamic_modules`
+fatals on anything still unloadable, which is right for an exe that owns its process and wrong for
+a library: the pending list it inherits is the HOST's, and a module the host could not load is not
+this library's to kill the process over. A guest calls `jit_lib_finalize_dynamic_modules` instead -
+retry, report, carry on - and a module this library actually needs still surfaces, as a null
+`
_create` carrying the reason.
+
+**Nothing calls the shutdown functions for a library.** A jitted `SimFunction` gets a zeroed
+`FuncInfo` (`jit_lib`'s `registerJitFunction`, `src/builtin/module_jit.cpp`), so
+`Context::runShutdownScript` - which selects on `FuncInfo::flag_shutdown`, set only by the
+interpreter's debug-info builder - finds none. `
_destroy` therefore emits the program's
+`[finalize]` / `[shutdown]` calls itself, the same way `
_create` emits its `[init]` calls. The
+process-level drain is the host's explicit, final `
_shutdown_runtime()` - never automatic, since
+a library cannot know whether the process is done with daslang, and an `atexit` hook would need
+process-global state to decide. It is a no-op unless this library's guard says it owns the runtime.
+
+Every linked artifact leaves through one emitter, `write_artifact` (`llvm_jit_common.das`): the
+`JitArtifact` kind picks the file name, the linker flavor (an archiver for a static library, no
+`-shared` for an exe) and the target CPU. A `jit_dll` always targets this host - it only ever runs
+on the box that emitted it - while a shipped artifact takes the caller's `use_host_cpu`: generic
+and redistributable by default, host-specific only when the build carries `[llvm_code]` kernels,
+whose tuner-generated IR a generic target refuses to legalize.
+
+`--jit-opt-level` (CLI, over `policies.jit_opt_level`, default 3) drives both the optimize
+pipeline and the DLL path's codegen-side target machine. A shipped artifact and AOT-object
+emission (`emit_object_only`) deliberately stay at codegen level 3: shipped artifacts are not
+content-addressed, so a tier change there has no cache guard to catch it. At level 0 the
+injected tune-policy default becomes `fallback` (`jit_cli_opt_level()` in `llvm_tune.das`):
+tune winners are raced under O3 codegen, so an O0 run cannot represent them and must not block
+on the tuner to mint them.
diff --git a/modules/dasLLVM/README.md b/modules/dasLLVM/README.md
index cd29e509c4..3c2bbec2d6 100644
--- a/modules/dasLLVM/README.md
+++ b/modules/dasLLVM/README.md
@@ -81,6 +81,72 @@ cached DLL for instant execution.
- By default, the `dll` is stored in `.jitted_scripts/`.
- This can be changed using `jit_output_path`.
+## Native library with a C API (`-lib`)
+`-lib` emits a native library plus the C header a host calls it through, so a
+program that only needs to *call* one script links no daslang API:
+
+```sh
+./bin/daslang -lib script.das -output build/script
+```
+
+writes `build/script.so` (`.dylib` / `.dll`) and `build/script.h`. Add
+`-- --jit-lib-static` for a `.a` / `.lib` archive instead; the generated header
+records what a host then links.
+
+Which functions cross the boundary, in three forms: `[export_c]` marks them one
+at a time; `-- --jit-lib-export-marked` takes whatever the program already marks
+`[export]`, so a script with a host API needs no new annotation; and
+`-lib-export-all` offers every public function of the entry module whose
+signature C can spell (naming the ones it skips). The first two are a selection
+only - a signature C cannot spell stays a hard error - while export-all skips it
+with a warning. `examples/c_api_library/` builds one library each way and loads
+all three at once through dasbind.
+
+`-- --jit-lib-bindings out/script_c.das` writes the daslang twin of the header
+alongside it: one `[extern(cdecl, late, ...)]` per entry point, plus a das struct
+per structure that crosses. A daslang host then `require`s that file instead of
+hand-writing the declarations or binding the C header through clang, and because
+the externs are `late` the bindings compile before the library exists - which is
+what lets a build system generate them as an ordinary output. The example's
+CMakeLists does exactly that for its three libraries. Every library gets the same four entry points,
+prefixed with the output name, so they always match the header they are declared in:
+
+```c
+script_ctx * script_create(void);
+void script_destroy(script_ctx * ctx);
+const char * script_last_error(script_ctx * ctx);
+void script_shutdown_runtime(void);
+```
+
+One instance owns one daslang context - its own globals, heap and string heap - and `_create`
+works on any thread. Several such libraries coexist in one process, as does one inside a host that
+registered the daslang modules itself: the first there registers the runtime, the rest bind to it.
+Scalars, `string`, pointers and enums cross by value; structures and the
+`float2`..`uint4` / `range` families cross as `const T *` and return through a
+trailing `T * out`. A daslang panic returns zero, leaves `out` untouched, and
+shows up in `script_last_error(ctx)` until the next call clears it.
+
+Codegen is the same as `-exe`: the same private wrappers, the same
+load-resolved globals, no DLL cache. Cross-compilation is not supported here -
+build the library on its target host.
+
+## Standalone contexts, and which tier writes one
+
+A standalone context is a script that carries its own `Context` and needs no
+compiler at run time. Two tiers write one, from the same script:
+
+- **AOT** - `daslang utils/aot/main.das -- -ctx script.das out/` emits C++ the
+ host compiles itself: `class Standalone : public Context` plus one method per
+ `[export]` function, next to the same C entry points `-lib` emits - one
+ describer writes both. The host owns the build, so it cross-compiles
+ anywhere its own toolchain reaches.
+- **LLVM** - `-lib` and `-exe` above emit the binary directly, no C++ compiler
+ in the loop and no daslang sources shipped. `-exe` can cross-compile, since
+ codegen picks the target triple; `-lib` is host-only.
+
+Both answer the same C API, so a host can move between them without editing a
+call site. `skills/cpp_integration.md` covers choosing one.
+
## Cross-compilation (WebAssembly)
The JIT pipeline can emit a non-host target instead of running on the host.
The supported cross-target is `wasm32-unknown-emscripten` (the default when no
diff --git a/modules/dasLLVM/REVIEW.md b/modules/dasLLVM/REVIEW.md
index 7b9c8659e5..995d4c6e96 100644
--- a/modules/dasLLVM/REVIEW.md
+++ b/modules/dasLLVM/REVIEW.md
@@ -76,6 +76,14 @@ there.
(`ARCHITECTURE.md` sec.5). An unpinned compile-time file read serves stale macro output
from the module cache until an unrelated source file changes - silently.
+- **A diff that adds a `-lib` entry point, or work to one, keeps the C boundary's three promises:
+ a raise reaches the caller as a return value and never an unwind, `
_create` answers null
+ rather than aborting, and `
_destroy` runs what the runtime's own shutdown cannot find**
+ (`ARCHITECTURE_LIB.md#lib-runtime-scope`). A library is called by code that cannot catch anything.
+
+- **A `-lib` build that writes no artifact exits non-zero.** A build rule reads the exit code, and
+ a silent success lets it link the previous run's library against this run's header.
+
- **A change to a `[tune]`-family annotation is reviewed with `skills/tune.md`.**
- **A change to the tune framework - `daslib/llvm_tune.das`, its tests, or the descriptor and
diff --git a/modules/dasLLVM/daslib/jit_standalone.das b/modules/dasLLVM/daslib/jit_standalone.das
new file mode 100644
index 0000000000..624aef656a
--- /dev/null
+++ b/modules/dasLLVM/daslib/jit_standalone.das
@@ -0,0 +1,327 @@
+options gen2
+options indenting = 4
+options no_unused_block_arguments = false
+options no_unused_function_arguments = false
+options strict_smart_pointers = false
+options relaxed_pointer_const
+options unsafe_table_lookup = false
+options no_global_variables = false
+
+module jit_standalone shared private
+
+require llvm/daslib/llvm_boost
+require llvm/daslib/llvm_jit
+require llvm/daslib/llvm_jit_common
+require llvm/daslib/llvm_dll_utils
+require llvm/daslib/llvm_dsl
+require daslib/ast_boost
+require daslib/option
+require daslib/c_api_header
+require daslib/safe_addr
+require daslib/defer
+
+
+def public lib_link_note(static_lib : bool; link_whole_lib : bool) : string {
+ let windows = get_platform_name() == "windows"
+ let rt_static = windows ? "libDaScript_runtime.lib" : "liblibDaScript_runtime.a"
+ let cc_static = windows ? "libDaScript.lib" : "liblibDaScript.a"
+ let rt_shared = windows ? "libDaScriptDyn_runtime.dll" : (get_platform_name() == "darwin" ? "liblibDaScriptDyn_runtime.dylib" : "liblibDaScriptDyn_runtime.so")
+ if (static_lib) {
+ let whole = link_whole_lib ? " Add /lib/{cc_static} too - this library\nregisters a module that lives in the compiler library." : ""
+ let sys = windows ? "" : " Also link the platform libraries the runtime needs:\n-lpthread -ldl -lm -lstdc++."
+ return "Link this archive plus /lib/{rt_static}.{whole}{sys}"
+ }
+ let whole = link_whole_lib ? " It also needs the compiler library beside it - this library\nregisters a module that lives there." : ""
+ return "Link this shared library. At load it needs {rt_shared}, which it looks for beside itself,\nin ../lib, and in /lib; otherwise put it on the loader path.{whole}"
+}
+
+
+def public write_das_bindings(var exports : array; names : CNames;
+ input_path, output_path, requested : string; static_lib : bool) : bool {
+ let bindings_path = requested == "auto" ? "{output_path}_c.das" : requested
+ if (static_lib) {
+ to_log(LOG_WARNING, "LLVM LIB: --jit-lib-bindings names a shared library - a static archive has nothing for dasbind to load\n")
+ }
+ let b = DasBindings(linux_path = "{output_path}.so", macos_path = "{output_path}.dylib",
+ windows_path = "{output_path}.dll")
+ let cmd = "daslang -lib {input_path} -output {output_path}"
+ if (!emit_das_bindings(exports, names, b, cmd, bindings_path)) {
+ to_log(LOG_ERROR, "LLVM LIB: the library is written but its daslang bindings are not - {bindings_path}\n")
+ return false
+ }
+ return true
+}
+
+
+def public new_function(ctx : LLVMContextRef; name : string; typ : LLVMTypeRef; exported : bool) : tuple {
+ let fn = LLVMAddFunctionWithType(g_mod, name, typ)
+ if (exported) {
+ set_public_linkage(fn)
+ } else {
+ set_private_linkage(fn)
+ }
+ let b = LLVMCreateBuilderInContext(ctx)
+ LLVMPositionBuilderAtEnd(b, LLVMAppendBasicBlockInContext(ctx, fn, "entry"))
+ return (fn = fn, builder = b)
+}
+
+
+struct private ThunkAbi {
+ arg_types : array
+ arg_loads : array
+ c_types : array
+ slot_type : LLVMOpaqueType?
+ has_slot : bool
+}
+
+
+//! The three type lists one thunk needs: what the impl takes (das ABI), what C hands over, and
+//! the result slot. `arg_loads[i]` marks a C pointer the impl wants BY VALUE - a vector or a
+//! range - so that argument arrives as `*(T *)incoming`, not as the pointer itself.
+def private thunk_abi(var e : CExport; var types : PrimitiveTypes?) : ThunkAbi {
+ var abi : ThunkAbi
+ abi.c_types |> push(types.LLVMVoidPtrType())
+ for (arg, p in e.fn.arguments, e.params) {
+ abi.arg_types |> push(type_to_llvm_abi_type(arg._type))
+ abi.arg_loads |> push(p.by_pointer && !p.pointer_in_impl)
+ abi.c_types |> push(p.by_pointer ? types.LLVMVoidPtrType()
+ : (arg._type.isBool ? types.t_int8 : type_to_llvm_abi_type(arg._type)))
+ }
+ if (e.result.via_out) {
+ abi.c_types |> push(types.LLVMVoidPtrType())
+ }
+ if (!e.fn.result.isVoid) {
+ abi.has_slot = true
+ abi.slot_type = (e.result.cmres ? types.LLVMVoidPtrType() : type_to_llvm_abi_type(e.fn.result))
+ }
+ return <- abi
+}
+
+
+def private frame_type(var abi : ThunkAbi; ctx : LLVMContextRef) : LLVMOpaqueType? {
+ var fields <- [for (t in abi.arg_types); t]
+ if (abi.has_slot) {
+ fields |> push(abi.slot_type)
+ }
+ if (fields |> empty()) {
+ fields |> push(g_prim_t.t_int32)
+ }
+ return LLVMStructTypeInContext(ctx, array_data_ptr(fields), uint(length(fields)), 0)
+}
+
+
+def private emit_trampoline(ctx : LLVMContextRef; var e : CExport; var uids : UidNodes?;
+ var types : PrimitiveTypes?; var abi : ThunkAbi; ft : LLVMOpaqueType?) : LLVMOpaqueValue? {
+ let impl_name = uids.get_dll_fn_name_ptr(e.fn).impl()
+ var impl = LLVMGetNamedFunction(g_mod, impl_name)
+ if (impl == null) {
+ return null
+ }
+ let tramp_type = jit_fn_type($(ctx, frame : void?) : void {})
+ let made = new_function(ctx, "__das_lib_tramp_{e.c_name}", tramp_type, false)
+ let b = made.builder
+ defer() {
+ LLVMDisposeBuilder(b)
+ }
+ var ctx_arg = LLVMGetParam(made.fn, 0u)
+ var frame = LLVMBuildPointerCast(b, LLVMGetParam(made.fn, 1u), LLVMPointerType(ft, 0u), "frame")
+ var args : array
+ args |> reserve(length(abi.arg_types) + 2)
+ for (at, i in abi.arg_types, count()) {
+ let slot = LLVMBuildStructGEP2(b, ft, frame, uint(i), "arg_{i}")
+ args |> push(LLVMBuildLoad2(b, at, slot, "argv_{i}"))
+ }
+ args |> push(ctx_arg)
+ let slot_index = uint(length(abi.arg_types))
+ if (e.result.cmres) {
+ var slot_ptr = LLVMBuildStructGEP2(b, ft, frame, slot_index, "res_ptr")
+ args |> push(LLVMBuildLoad2(b, types.LLVMVoidPtrType(), slot_ptr, "res"))
+ }
+ var ret = LLVMBuildCall2(b, g_fn_types[impl_name], impl, args, "")
+ if (abi.has_slot && !e.result.cmres) {
+ LLVMBuildStore(b, ret, LLVMBuildStructGEP2(b, ft, frame, slot_index, "res"))
+ }
+ LLVMBuildRetVoid(b)
+ return made.fn
+}
+
+
+def private c_return_type(var e : CExport; var types : PrimitiveTypes?) : LLVMTypeRef {
+ if (e.result.via_out || e.fn.result.isVoid) {
+ return types.t_void
+ }
+ return e.fn.result.isBool ? types.t_int8 : type_to_llvm_abi_type(e.fn.result)
+}
+
+
+def private store_thunk_args(b : LLVMBuilderRef; var e : CExport; var abi : ThunkAbi;
+ fn : LLVMOpaqueValue?; ft : LLVMOpaqueType?; var types : PrimitiveTypes?; frame : LLVMOpaqueValue?) {
+ for (arg, p, i in e.fn.arguments, e.params, count()) {
+ var incoming = LLVMGetParam(fn, uint(i + 1))
+ var slot = LLVMBuildStructGEP2(b, ft, frame, uint(i), "in_{i}")
+ if (abi.arg_loads[i]) {
+ var typed = LLVMBuildPointerCast(b, incoming, LLVMPointerType(abi.arg_types[i], 0u), "")
+ incoming = LLVMBuildLoadData2Aligned(b, abi.arg_types[i], typed, arg._type.alignOf, "val_{i}")
+ } elif (p.by_pointer) {
+ incoming = LLVMBuildPointerCast(b, incoming, abi.arg_types[i], "")
+ } elif (arg._type.isBool) {
+ incoming = LLVMBuildICmp(b, LLVMIntPredicate.LLVMIntNE, incoming, types.ConstI8(int8(0)), "b_{i}")
+ }
+ LLVMBuildStore(b, incoming, slot)
+ }
+}
+
+
+def public emit_thunk(ctx : LLVMContextRef; var e : CExport; var uids : UidNodes?; var types : PrimitiveTypes?;
+ export_all : bool) : Option {
+ var abi <- thunk_abi(e, types)
+ let ft = frame_type(abi, ctx)
+ let tramp = emit_trampoline(ctx, e, uids, types, abi, ft)
+ if (tramp == null) {
+ if (export_all) {
+ to_log(LOG_WARNING, "LLVM LIB: skipping `{e.fn.name}`: no jitted body to export to C\n")
+ return some(true)
+ }
+ to_log(LOG_ERROR, "LLVM LIB: no jitted body for `{e.fn.name}` - it cannot be exported to C\n")
+ return none(type)
+ }
+ if (LLVMGetNamedFunction(g_mod, e.c_name) != null) {
+ to_log(LOG_ERROR, "LLVM LIB: C symbol `{e.c_name}` is already emitted; rename one with [export_c(name = \"...\")]\n")
+ return none(type)
+ }
+ let made = new_function(ctx, e.c_name, LLVMFunctionType(c_return_type(e, types), abi.c_types), true)
+ let b = made.builder
+ defer() {
+ LLVMDisposeBuilder(b)
+ }
+ var frame = LLVMBuildAlloca(b, ft, "frame")
+ var cmres_slot : LLVMOpaqueValue?
+ if (e.result.cmres) {
+ cmres_slot = LLVMBuildAlloca(b, LLVMArrayType(types.t_int8, uint(e.fn.result.sizeOf)), "result")
+ LLVMSetAlignment(cmres_slot, uint(e.fn.result.alignOf))
+ LLVMBuildStore(b, LLVMBuildPointerCast(b, cmres_slot, types.LLVMVoidPtrType(), ""),
+ LLVMBuildStructGEP2(b, ft, frame, uint(length(abi.arg_types)), "res_ptr"))
+ }
+ store_thunk_args(b, e, abi, made.fn, ft, types, frame)
+ let guard_type = jit_fn_type($(ctx, tramp, frame : void?) : int {})
+ var guard = declare_extern_fn("jit_lib_invoke_guarded", guard_type)
+ var guard_args = array(LLVMGetParam(made.fn, 0u),
+ LLVMBuildPointerCast(b, tramp, types.LLVMVoidPtrType(), ""),
+ LLVMBuildPointerCast(b, frame, types.LLVMVoidPtrType(), ""))
+ var ok = LLVMBuildCall2(b, guard_type, guard, guard_args, "ok")
+ if (!abi.has_slot) {
+ LLVMBuildRetVoid(b)
+ return some(false)
+ }
+ let ok_bb = LLVMAppendBasicBlockInContext(ctx, made.fn, "ok")
+ let fail_bb = LLVMAppendBasicBlockInContext(ctx, made.fn, "raised")
+ var cond = LLVMBuildICmp(b, LLVMIntPredicate.LLVMIntNE, ok, types.ConstI32(0ul), "ran")
+ LLVMBuildCondBr(b, cond, ok_bb, fail_bb)
+ let slot_index = uint(length(abi.arg_types))
+ LLVMPositionBuilderAtEnd(b, ok_bb)
+ if (e.result.via_out) {
+ var out_ptr = LLVMGetParam(made.fn, uint(length(e.params) + 1))
+ var source = e.result.cmres ? cmres_slot : LLVMBuildStructGEP2(b, ft, frame, slot_index, "res")
+ LLVMBuildMemCpy(b, out_ptr, 1u, source, uint(e.fn.result.alignOf), types.ConstI64(uint64(e.fn.result.sizeOf)))
+ LLVMBuildRetVoid(b)
+ } else {
+ var value = LLVMBuildLoad2(b, abi.slot_type, LLVMBuildStructGEP2(b, ft, frame, slot_index, "res"), "value")
+ LLVMBuildRet(b, e.fn.result.isBool ? LLVMBuildZExt(b, value, types.t_int8, "b") : value)
+ }
+ LLVMPositionBuilderAtEnd(b, fail_bb)
+ if (e.result.via_out || e.fn.result.isVoid) {
+ LLVMBuildRetVoid(b)
+ } else {
+ LLVMBuildRet(b, LLVMConstNull(c_return_type(e, types)))
+ }
+ return some(false)
+}
+
+
+def private emit_shutdown_trampoline(ctx : LLVMContextRef; var uids : UidNodes?;
+ var funcs : array) : LLVMOpaqueValue? {
+ var shutdown_fns <- [for (fn in funcs); fn; where fn.flags.shutdown]
+ if (shutdown_fns |> empty()) {
+ return null
+ }
+ let made = new_function(ctx, "__das_lib_ctx_shutdown", jit_fn_type($(ctx, frame : void?) : void {}), false)
+ let b = made.builder
+ defer() {
+ LLVMDisposeBuilder(b)
+ }
+ var ctx_arg = LLVMGetParam(made.fn, 0u)
+ for (fn in shutdown_fns) {
+ let impl_name = uids.get_dll_fn_name(fn).impl()
+ var impl = LLVMGetNamedFunction(g_mod, impl_name)
+ if (impl != null) {
+ LLVMBuildCall2(b, g_fn_types[impl_name], impl, array(ctx_arg), "")
+ }
+ }
+ LLVMBuildRetVoid(b)
+ return made.fn
+}
+
+
+def private emit_destroy(ctx : LLVMContextRef; var types : PrimitiveTypes?; prefix : string;
+ shutdown_tramp : LLVMOpaqueValue?) {
+ let void_ptr = types.LLVMVoidPtrType()
+ let made = new_function(ctx, "{prefix}_destroy", jit_fn_type($(ctx : void?) : void {}), true)
+ let b = made.builder
+ defer() {
+ LLVMDisposeBuilder(b)
+ }
+ var ctx_arg = LLVMGetParam(made.fn, 0u)
+ if (shutdown_tramp != null) {
+ var guard = declare_extern_fn("jit_lib_invoke_guarded", jit_fn_type($(ctx, tramp, frame : void?) : int {}))
+ LLVMBuildCall2(b, jit_fn_type($(ctx, tramp, frame : void?) : int {}), guard,
+ array(ctx_arg, LLVMBuildPointerCast(b, shutdown_tramp, void_ptr, ""), LLVMConstNull(void_ptr)), "")
+ }
+ let destroy_type = jit_fn_type($(ctx : void?) : void {})
+ var destroy = declare_extern_fn("jit_destroy_standalone_ctx", destroy_type)
+ LLVMBuildCall2(b, destroy_type, destroy, array(ctx_arg), "")
+ LLVMBuildRetVoid(b)
+}
+
+
+def private emit_last_error(ctx : LLVMContextRef; var types : PrimitiveTypes?; prefix : string;
+ error_global : LLVMOpaqueValue?) {
+ let void_ptr = types.LLVMVoidPtrType()
+ let typ = jit_fn_type($(ctx : void?) : void? {})
+ let made = new_function(ctx, "{prefix}_last_error", typ, true)
+ let b = made.builder
+ defer() {
+ LLVMDisposeBuilder(b)
+ }
+ var ctx_arg = LLVMGetParam(made.fn, 0u)
+ let from_slot = LLVMAppendBasicBlockInContext(ctx, made.fn, "from_slot")
+ let from_ctx = LLVMAppendBasicBlockInContext(ctx, made.fn, "from_ctx")
+ var no_ctx = LLVMBuildICmp(b, LLVMIntPredicate.LLVMIntEQ, ctx_arg, LLVMConstNull(void_ptr), "no_ctx")
+ LLVMBuildCondBr(b, no_ctx, from_slot, from_ctx)
+ LLVMPositionBuilderAtEnd(b, from_slot)
+ LLVMBuildRet(b, LLVMBuildLoad2(b, void_ptr, error_global, "create_error"))
+ LLVMPositionBuilderAtEnd(b, from_ctx)
+ LLVMBuildRet(b, LLVMBuildCall2(b, typ, declare_extern_fn("jit_lib_last_error", typ), array(ctx_arg), ""))
+}
+
+
+def private emit_shutdown_runtime(ctx : LLVMContextRef; var types : PrimitiveTypes?; prefix : string;
+ guard_global : LLVMOpaqueValue?) {
+ let made = new_function(ctx, "{prefix}_shutdown_runtime", jit_fn_type($() : void {}), true)
+ let b = made.builder
+ defer() {
+ LLVMDisposeBuilder(b)
+ }
+ let shutdown_type = jit_fn_type($(guard : int) : void {})
+ var guard = LLVMBuildLoad2(b, types.t_int32, guard_global, "guard")
+ LLVMBuildCall2(b, shutdown_type, declare_extern_fn("jit_lib_shutdown", shutdown_type), array(guard), "")
+ LLVMBuildRetVoid(b)
+}
+
+
+def public emit_fixed_entries(ctx : LLVMContextRef; var types : PrimitiveTypes?; var uids : UidNodes?;
+ var funcs : array; prefix : string;
+ guard_global, error_global : LLVMOpaqueValue?) {
+ emit_destroy(ctx, types, prefix, emit_shutdown_trampoline(ctx, uids, funcs))
+ emit_last_error(ctx, types, prefix, error_global)
+ emit_shutdown_runtime(ctx, types, prefix, guard_global)
+}
diff --git a/modules/dasLLVM/daslib/llvm_dll_utils.das b/modules/dasLLVM/daslib/llvm_dll_utils.das
index c37d74231c..b1b01b1930 100644
--- a/modules/dasLLVM/daslib/llvm_dll_utils.das
+++ b/modules/dasLLVM/daslib/llvm_dll_utils.das
@@ -133,7 +133,9 @@ class public UidNodes {
def private get_base(ptr : void?; hint : string) {
if (!(id_map |> key_exists(ptr))) {
- panic("Key not found for {hint}")
+ let who = thisFunc != null ? "{describe(thisFunc.at)}: {thisFunc.name}" : "a global initializer"
+ panic("LLVM: {who} is outside the JIT function set - it fell back to the interpreter, " +
+ "so its `{hint}` has no id and cannot be emitted")
return DllName()
}
let id = unsafe(id_map[ptr])
@@ -301,17 +303,31 @@ class public DLLHandle {
}
-def public add_obj_extension(path : string) {
- return "{path}.o"
-}
-def public add_dll_extension(path : string) {
- return "{path}.dll"
+enum public JitArtifact {
+ object
+ jit_dll
+ exe
+ shared_lib
+ static_lib
}
-def public add_exe_extension(path : string) {
- return "{path}.exe"
+
+def public artifact_path(path : string; kind : JitArtifact) : string {
+ let plat = get_platform_name()
+ var suffix = "o"
+ if (kind == JitArtifact.jit_dll) {
+ suffix = "dll"
+ } elif (kind == JitArtifact.exe) {
+ suffix = "exe"
+ } elif (kind == JitArtifact.static_lib) {
+ suffix = plat == "windows" ? "lib" : "a"
+ } elif (kind == JitArtifact.shared_lib) {
+ suffix = plat == "windows" ? "dll" : (plat == "darwin" ? "dylib" : "so")
+ }
+ return "{path}.{suffix}"
}
+
def public get_dll_by_path(path : string) : DLLHandle? {
- return new DLLHandle(handle = load_dynamic_library(add_dll_extension(path)))
+ return new DLLHandle(handle = load_dynamic_library(artifact_path(path, JitArtifact.jit_dll)))
}
diff --git a/modules/dasLLVM/daslib/llvm_exe.das b/modules/dasLLVM/daslib/llvm_exe.das
index 6bea394bc3..28b05d00d6 100644
--- a/modules/dasLLVM/daslib/llvm_exe.das
+++ b/modules/dasLLVM/daslib/llvm_exe.das
@@ -15,6 +15,8 @@ require llvm/daslib/llvm_jit
require llvm/daslib/llvm_jit_intrin
require llvm/daslib/llvm_jit_common
require llvm/daslib/llvm_dll_utils
+require llvm/daslib/jit_standalone
+require daslib/c_api_header
require daslib/ast_boost
require daslib/templates_boost
require daslib/macro_boost
@@ -38,6 +40,7 @@ bitfield TabOperation {
}
+var private g_lib_registration_once = false
class public CollectExternVisitor : AstVisitor {
@@ -126,19 +129,19 @@ class public CollectExternVisitor : AstVisitor {
registered_modules[m] = true
}
} else {
- // Register $ and strings unless inject_main's static sweep already emitted the (non-idempotent) call.
- if (!(g_exe_emitted_reg |> key_exists("jit_register_Module_BuiltIn"))) {
- g_exe_emitted_reg["jit_register_Module_BuiltIn"] = true
- var reg_builtin = LLVMAddFunctionWithType(g_mod, "jit_register_Module_BuiltIn", register_mod_type)
- LLVMBuildCall2(ib, register_mod_type, reg_builtin, array(), "")
- }
- registered_modules["$"] = true
- if (!(g_exe_emitted_reg |> key_exists("jit_register_Module_Strings"))) {
- g_exe_emitted_reg["jit_register_Module_Strings"] = true
- var reg_strings = LLVMAddFunctionWithType(g_mod, "jit_register_Module_Strings", register_mod_type)
- LLVMBuildCall2(ib, register_mod_type, reg_strings, array(), "")
+ let once_type = jit_fn_type($(name, reg : void?) : void? {})
+ var once = declare_extern_fn("jit_register_module_once", once_type)
+ for (pair in fixed_array(fixed_array("$", "jit_register_Module_BuiltIn"),
+ fixed_array("strings", "jit_register_Module_Strings"))) {
+ if (!(g_exe_emitted_reg |> key_exists(pair[1]))) {
+ g_exe_emitted_reg[pair[1]] = true
+ var reg_fn = declare_extern_fn(pair[1], register_mod_type)
+ LLVMBuildCall2(ib, once_type, once,
+ array(get_string_constant_ptr(ib, pair[0]),
+ LLVMBuildPointerCast(ib, reg_fn, g_prim_t.LLVMVoidPtrType(), "")), "")
+ }
+ registered_modules[pair[0]] = true
}
- registered_modules["strings"] = true
}
return ib
}
@@ -315,14 +318,23 @@ class public CollectExternVisitor : AstVisitor {
if (mod_name == "ast_core" || mod_name == "ast" || mod_name == "network_core" || mod_name == "network") {
needs_whole_lib = true
}
- // One call per thunk process-wide (not idempotent); get-or-add avoids a silent rename to an undefined symbol.
return if (g_exe_emitted_reg |> key_exists(reg_fn_name))
g_exe_emitted_reg[reg_fn_name] = true
var reg_fn = LLVMGetNamedFunction(g_mod, reg_fn_name)
if (reg_fn == null) {
reg_fn = LLVMAddFunctionWithType(g_mod, reg_fn_name, register_mod_type)
}
- LLVMBuildCall2(init_builder, register_mod_type, reg_fn, array(), "")
+ if (!g_lib_registration_once) {
+ LLVMBuildCall2(init_builder, register_mod_type, reg_fn, array(), "")
+ return
+ }
+ let once_type = jit_fn_type($(name, reg : void?) : void? {})
+ var once = LLVMGetNamedFunction(g_mod, "jit_register_module_once")
+ if (once == null) {
+ once = LLVMAddFunctionWithType(g_mod, "jit_register_module_once", once_type)
+ }
+ LLVMBuildCall2(init_builder, once_type, once,
+ array(get_string_constant_ptr(init_builder, mod_name), reg_fn), "")
}
def make_call(expr : ExprCallFunc?) {
@@ -673,12 +685,14 @@ def collect_external_functions(standalone_context : LLVMOpaqueValue?; ctx : LLVM
}
}
if (!empty(extern_resolver.registered_modules) || !empty(used_modules)) {
- let init_done_type = LLVMFunctionType(t.t_void, array())
+ let init_done_type = LLVMFunctionType(t.t_void, array(t.t_int32))
var jit_init_done = LLVMGetNamedFunction(g_mod, "jit_initialize_modules_done")
if (jit_init_done == null) {
jit_init_done = LLVMAddFunctionWithType(g_mod, "jit_initialize_modules_done", init_done_type)
}
- LLVMBuildCall2(ib, init_done_type, jit_init_done, array(), "")
+ var lib_guard = LLVMGetNamedGlobal(g_mod, LIB_GUARD_GLOBAL)
+ var guard_arg = lib_guard == null ? t.ConstI32(0ul) : LLVMBuildLoad2(ib, t.t_int32, lib_guard, "guard")
+ LLVMBuildCall2(ib, init_done_type, jit_init_done, array(guard_arg), "")
}
// Wasm: fill per-(handled-type, field) offset globals from the TARGET runtime's annotations (now registered) — each access reads a global, not a host-baked constant.
if (!empty(g_handled_field_offset_globals)) {
@@ -1008,24 +1022,25 @@ def private emit_module_registration(m : Module?; dynamic_modules : table(), "")
+ var reg_fn = declare_extern_fn(reg_fn_name, register_mod_type)
+ let once_type = jit_fn_type($(name, reg : void?) : void? {})
+ var once = declare_extern_fn("jit_register_module_once", once_type)
+ LLVMBuildCall2(builder, once_type, once,
+ array(get_string_constant_ptr(builder, mod_name),
+ reg_fn), "")
}
// Creates main function that initializes a standalone JIT context, registers
// all compiled functions, runs init scripts, then calls the program entry point.
-def public inject_main(program_context : Context?; ctx : LLVMContextRef; // nolint:STYLE037,STYLE038 — the standalone-exe entry emitter: one emission block per runtime feature, in startup order
- prog : Program ?; entry_point : string; mod : LLVMOpaqueModule?;
- var types : PrimitiveTypes?, var uids : UidNodes?; strict : bool;
- register_all_modules : bool = false) : tuple {
- g_exe_emitted_reg |> clear() // per-codegen-run: one registration call per module thunk
- let builder = LLVMCreateBuilder()
- defer() {
- LLVMDisposeBuilder(builder)
- }
+struct public StandaloneFunctions {
+ funcs : array
+ used_modules : table
+ any_pinvoke : bool
+ ok : bool
+}
+
+
+def public collect_standalone_functions(prog : Program?; strict : bool) : StandaloneFunctions {
// Collect ALL used functions from the program (not just JIT-compiled ones from `funcs`).
// There shouldn't be any `das` functions in standalone_exe, otherwise it
// will crash.
@@ -1051,56 +1066,39 @@ def public inject_main(program_context : Context?; ctx : LLVMContextRef; // noli
}
if (strict && has_no_jit) {
to_log(LOG_ERROR, "Cannot build standalone exe: some functions are no_jit in strict mode\n")
- return (fn = null, link_whole_lib = false)
+ return <- StandaloneFunctions(funcs <- funcs, used_modules <- used_modules, any_pinvoke = any_pinvoke)
}
+ return <- StandaloneFunctions(funcs <- funcs, used_modules <- used_modules, any_pinvoke = any_pinvoke, ok = true)
+}
- // --jit-register-all-modules: compiler-driver exes may recompile targets needing UnitTest at
- // runtime. Force it only in main()'s dynamic-module loop below — NOT via used_modules, else
- // initialize_modules() emits a second register call ("Module 'UnitTest' already created").
- var force_dynamic_modules : table
- if (register_all_modules) {
- force_dynamic_modules["UnitTest"] = true
- }
+def private find_exe_entry(var funcs : array; var uids : UidNodes?; entry_point : string) : tuple {
var start_fn_name = ""
var no_return = true
var bool_return = false
for (fn in funcs) {
if (fn.name == entry_point && fn.arguments.empty()) {
assume fnmna = uids.get_dll_fn_name(fn).impl()
- if (fn.result.isVoid) {
- start_fn_name = fnmna
- no_return = true
- } elif (fn.result.baseType == Type.tInt) {
- start_fn_name = fnmna
- no_return = false
- } elif (fn.result.baseType == Type.tBool) {
- start_fn_name = fnmna
- no_return = false
- bool_return = true
- }
+ let is_void = fn.result.isVoid
+ let is_bool = fn.result.baseType == Type.tBool
+ let takes_it = is_void || is_bool || fn.result.baseType == Type.tInt
+ start_fn_name = takes_it ? fnmna : start_fn_name
+ no_return = takes_it ? is_void : no_return
+ bool_return = takes_it ? is_bool : bool_return
}
}
- if (start_fn_name |> empty()) {
- to_log(LOG_ERROR, "entrypoint `{entry_point}()` not found in input file.\n")
- return (fn = null, link_whole_lib = false)
- }
- let main_fn_type = LLVMFunctionType(types.t_int32,
- fixed_array(
- types.t_int32, // argc
- types.LLVMVoidPtrType() // argv
- )
- )
- // wasm32-emscripten: emcc's libstandalonewasm already defines `main`,
- // emit `__main_argc_argv` instead and let crt1 chain through.
- let main_sym = g_target_is_wasm ? "__main_argc_argv" : "main"
- let main_fn = LLVMAddFunctionWithType(mod, main_sym, main_fn_type)
- let entry = LLVMAppendBasicBlockInContext(ctx, main_fn, "entry")
- LLVMPositionBuilderAtEnd(builder, entry)
+ return (name = start_fn_name, no_return = no_return, bool_return = bool_return)
+}
+
+def public emit_standalone_runtime_init(builder : LLVMBuilderRef; prog : Program?; var types : PrimitiveTypes?; // nolint:STYLE038 - one linear registration sequence, in startup order; a split would hide which step runs when
+ register_all_modules : bool; force_dynamic_modules : table;
+ var used_modules : table&;
+ var dynamic_modules : table&;
+ guest : bool = false) : bool {
+ g_exe_emitted_reg |> clear() // per-codegen-run: one registration call per module thunk
// Collect dynamic module names — used by CollectExternVisitor to skip dlopen'd modules
- var dynamic_modules : table
for_each_registered_dynamic_module() $(_path, _mod_name, das_name) {
dynamic_modules[das_name] = true
}
@@ -1199,30 +1197,19 @@ def public inject_main(program_context : Context?; ctx : LLVMContextRef; // noli
LLVMBuildCall2(builder, init_modules_type, init_modules_fn, array(), "")
// Dynamic modules load Quiet (sibling DT_NEEDED ordering): retry after the last registration
- // and fatal on anything still unloadable, naming the module + dlopen error. Native only — on
- // wasm those modules also register via static thunks, so the failed resolve is BY DESIGN.
if (ships_dynamic && !g_target_is_wasm) {
let fin_dyn_type = LLVMFunctionType(types.t_void, array())
- var fin_dyn = LLVMAddFunctionWithType(g_mod, "jit_finalize_dynamic_modules", fin_dyn_type)
+ let fin_dyn_name = guest ? "jit_lib_finalize_dynamic_modules" : "jit_finalize_dynamic_modules"
+ var fin_dyn = LLVMAddFunctionWithType(g_mod, fin_dyn_name, fin_dyn_type)
LLVMBuildCall2(builder, fin_dyn_type, fin_dyn, array(), "")
}
- // Init argc, argv
- let set_cmd_args_type = LLVMFunctionType(types.t_void,
- fixed_array(
- types.t_int32, // argc
- types.LLVMVoidPtrType() // argv
- )
- )
- var jit_set_cmd_args = LLVMAddFunctionWithType(
- g_mod, "jit_set_command_line_arguments", set_cmd_args_type
- )
- let argc = LLVMGetParam(main_fn, 0 |> uint); // argc
- let argv = LLVMGetParam(main_fn, 1 |> uint); // argv
- LLVMBuildCall2(builder, set_cmd_args_type,
- jit_set_cmd_args, fixed_array(argc, argv), "")
+ return ships_dynamic
+}
+def public emit_create_standalone_ctx(builder : LLVMBuilderRef; program_context : Context?; prog : Program?;
+ var types : PrimitiveTypes?; any_pinvoke : bool) : LLVMOpaqueValue? {
// Determine the context stack size from options (mirrors Program::getContextStackSize)
var context_stack_size = uint64(prog.policies.stack)
let stack_opt = find_arg(prog._options, "stack")
@@ -1257,7 +1244,17 @@ def public inject_main(program_context : Context?; ctx : LLVMContextRef; // noli
types.ConstI64(context_stack_size)
)
let global_context = LLVMBuildCall2(builder, create_ctx_type, jit_create_context, params, "")
+ return global_context
+}
+
+def public emit_standalone_context_init(builder : LLVMBuilderRef; fusion_builder : LLVMBuilderRef; // nolint:STYLE038 - one linear startup sequence sharing the context value; a split would hide which step runs when
+ program_context : Context ?; ctx : LLVMContextRef; prog : Program?;
+ mod : LLVMOpaqueModule?; var types : PrimitiveTypes?; var uids : UidNodes?;
+ var funcs : array; global_context : LLVMOpaqueValue?;
+ register_all_modules : bool; ships_dynamic : bool;
+ var used_modules : table&;
+ dynamic_modules : table) : bool {
// Pre-build init_globals so its function-pointer globals exist before
// collect_external_functions walks them (issue #2582). Actual init_globals(ctx)
// call is emitted further down at its runtime position in main_fn.
@@ -1325,9 +1322,9 @@ def public inject_main(program_context : Context?; ctx : LLVMContextRef; // noli
// Whole-compiler-lib link: register the fusion engine (needed by simulate). Skip on wasm —
// cross-link only sees libDaScript_runtime.a (no jit_register_fusion), and exes never read it (#2805).
if (needs_whole_lib && !g_target_is_wasm) {
- let reg_fusion_type = LLVMFunctionType(types.t_void, array())
- let reg_fusion_fn = LLVMAddFunctionWithType(g_mod, "jit_register_fusion", reg_fusion_type)
- LLVMBuildCall2(builder, reg_fusion_type, reg_fusion_fn, array(), "")
+ let reg_fusion_type = jit_fn_type($() : void {})
+ var reg_fusion_fn = declare_extern_fn("jit_register_fusion", reg_fusion_type)
+ LLVMBuildCall2(fusion_builder, reg_fusion_type, reg_fusion_fn, array(), "")
}
// void jit_register_standalone_variable ( Context * ctx, uint64_t index, const char * name, uint64_t mnh, uint64_t offset, int shared )
@@ -1357,10 +1354,14 @@ def public inject_main(program_context : Context?; ctx : LLVMContextRef; // noli
LLVMBuildCall2(builder, init_global_var_type, init_global_var, var_params, "")
}
// the sealed lookups are adopted right after the context is created - before any registration
- // call, so no JIT code can ever probe an unadopted table, however LLVM orders the calls
{
let resume_block = LLVMGetInsertBlock(builder)
- LLVMPositionBuilderBefore(builder, LLVMGetNextInstruction(global_context))
+ if (LLVMIsAInstruction(global_context) != null) {
+ LLVMPositionBuilderBefore(builder, LLVMGetNextInstruction(global_context))
+ } else {
+ let entry = LLVMGetEntryBasicBlock(LLVMGetBasicBlockParent(resume_block))
+ LLVMPositionBuilder(builder, entry, LLVMGetFirstInstruction(entry))
+ }
emit_exe_lookups(builder, ctx, types, global_context, collected.registered, program_context)
LLVMPositionBuilderAtEnd(builder, resume_block)
}
@@ -1421,6 +1422,72 @@ def public inject_main(program_context : Context?; ctx : LLVMContextRef; // noli
fixed_array(global_context, LLVMBuildPointerCast(builder, init_script_fn, types.LLVMVoidPtrType(), "")), "")
}
+ return needs_whole_lib
+}
+def public inject_main(program_context : Context?; ctx : LLVMContextRef; // nolint:STYLE037,STYLE038 — the standalone-exe entry emitter: one emission block per runtime feature, in startup order
+ prog : Program ?; entry_point : string; mod : LLVMOpaqueModule?;
+ var types : PrimitiveTypes?, var uids : UidNodes?; strict : bool;
+ register_all_modules : bool = false) : tuple {
+ let builder = LLVMCreateBuilder()
+ defer() {
+ LLVMDisposeBuilder(builder)
+ }
+ var std_fns <- collect_standalone_functions(prog, strict)
+ var funcs <- std_fns.funcs
+ var used_modules <- std_fns.used_modules
+ let any_pinvoke = std_fns.any_pinvoke
+ if (!std_fns.ok) {
+ return (fn = null, link_whole_lib = false)
+ }
+
+ var force_dynamic_modules : table
+ if (register_all_modules) {
+ force_dynamic_modules["UnitTest"] = true
+ }
+
+ let found = find_exe_entry(funcs, uids, entry_point)
+ let start_fn_name = found.name
+ let no_return = found.no_return
+ let bool_return = found.bool_return
+ if (start_fn_name |> empty()) {
+ to_log(LOG_ERROR, "entrypoint `{entry_point}()` not found in input file.\n")
+ return (fn = null, link_whole_lib = false)
+ }
+
+ let main_fn_type = LLVMFunctionType(types.t_int32,
+ fixed_array(
+ types.t_int32, // argc
+ types.LLVMVoidPtrType() // argv
+ )
+ )
+ let main_sym = g_target_is_wasm ? "__main_argc_argv" : "main"
+ let main_fn = LLVMAddFunctionWithType(mod, main_sym, main_fn_type)
+ let entry = LLVMAppendBasicBlockInContext(ctx, main_fn, "entry")
+ LLVMPositionBuilderAtEnd(builder, entry)
+
+ var dynamic_modules : table
+ let ships_dynamic = emit_standalone_runtime_init(builder, prog, types, register_all_modules,
+ force_dynamic_modules, used_modules, dynamic_modules)
+ let set_cmd_args_type = LLVMFunctionType(types.t_void,
+ fixed_array(
+ types.t_int32, // argc
+ types.LLVMVoidPtrType() // argv
+ )
+ )
+ var jit_set_cmd_args = LLVMAddFunctionWithType(
+ g_mod, "jit_set_command_line_arguments", set_cmd_args_type
+ )
+ let argc = LLVMGetParam(main_fn, 0 |> uint); // argc
+ let argv = LLVMGetParam(main_fn, 1 |> uint); // argv
+ LLVMBuildCall2(builder, set_cmd_args_type,
+ jit_set_cmd_args, fixed_array(argc, argv), "")
+
+
+ let global_context = emit_create_standalone_ctx(builder, program_context, prog, types, any_pinvoke)
+
+ let needs_whole_lib = emit_standalone_context_init(builder, builder, program_context, ctx, prog, mod,
+ types, uids, funcs, global_context, register_all_modules, ships_dynamic, used_modules, dynamic_modules)
+ var main_params = fixed_array(global_context)
// Match the interpreter's WebLoop: on wasm a program exporting `update` runs as init() once +
// update() per browser frame + shutdown() (the browser can't block in main's while-loop). `main`
// stays the desktop driver, bypassed here — so the same .das cross-compiles unchanged for web.
@@ -1510,3 +1577,150 @@ def public inject_main(program_context : Context?; ctx : LLVMContextRef; // noli
to_log(LOG_INFO, "LLVM JIT: standalone exe entry point generated '{start_fn_name}', {length(funcs)} functions\n")
return (fn = main_fn, link_whole_lib = needs_whole_lib)
}
+
+
+let private LIB_GUARD_GLOBAL = "__das_lib_init_guard"
+let private LIB_ENV_GLOBAL = "__das_lib_environment"
+let private LIB_ERROR_GLOBAL = "__das_lib_create_error"
+let private LIB_RUNTIME_INIT = "__das_lib_runtime_init"
+let private LIB_CTX_INIT = "__das_lib_ctx_init"
+var public g_lib_exports : array
+
+
+def private emit_lib_globals(var types : PrimitiveTypes?) {
+ var guard = LLVMAddGlobal(g_mod, types.t_int32, LIB_GUARD_GLOBAL)
+ set_private_linkage(guard)
+ LLVMSetInitializer(guard, types.ConstI32(0ul))
+ for (name in [LIB_ENV_GLOBAL, LIB_ERROR_GLOBAL]) {
+ var slot = LLVMAddGlobal(g_mod, types.LLVMVoidPtrType(), name)
+ set_private_linkage(slot)
+ LLVMSetInitializer(slot, LLVMConstNull(types.LLVMVoidPtrType()))
+ }
+}
+
+
+def private emit_runtime_init(ctx : LLVMContextRef; prog : Program?; var types : PrimitiveTypes?;
+ register_all_modules : bool; var used_modules : table&;
+ var dynamic_modules : table&) : tuple {
+ emit_lib_globals(types)
+ let void_fn_type = jit_fn_type($() : void {})
+ let made = new_function(ctx, LIB_RUNTIME_INIT, void_fn_type, false)
+ let fresh_init_modules = LLVMGetNamedFunction(g_mod, "initialize_modules") == null
+ var init_modules = declare_extern_fn("initialize_modules", void_fn_type)
+ if (fresh_init_modules) {
+ set_private_linkage(init_modules)
+ }
+ var force_dynamic_modules : table
+ if (register_all_modules) {
+ force_dynamic_modules["UnitTest"] = true
+ }
+ let ships = emit_standalone_runtime_init(made.builder, prog, types, register_all_modules,
+ force_dynamic_modules, used_modules, dynamic_modules, true)
+ return (fn = made.fn, builder = made.builder, ships_dynamic = ships)
+}
+
+
+def private emit_create(ctx : LLVMContextRef; program_context : Context?; prog : Program?; mod : LLVMOpaqueModule?;
+ var types : PrimitiveTypes?; var uids : UidNodes?; prefix : string;
+ var sf : StandaloneFunctions; runtime_init : LLVMOpaqueValue?;
+ runtime_builder : LLVMBuilderRef; register_all_modules : bool; ships_dynamic : bool;
+ dynamic_modules : table) : tuple {
+ let void_ptr = types.LLVMVoidPtrType()
+ var guard_global = LLVMGetNamedGlobal(g_mod, LIB_GUARD_GLOBAL)
+ var env_global = LLVMGetNamedGlobal(g_mod, LIB_ENV_GLOBAL)
+ var error_global = LLVMGetNamedGlobal(g_mod, LIB_ERROR_GLOBAL)
+
+ let ctx_init = new_function(ctx, LIB_CTX_INIT, jit_fn_type($(ctx, frame : void?) : void {}), false)
+ let made = new_function(ctx, "{prefix}_create", jit_fn_type($() : void? {}), true)
+ let b = made.builder
+ defer() {
+ LLVMDisposeBuilder(b)
+ LLVMDisposeBuilder(ctx_init.builder)
+ }
+ let run_once_type = jit_fn_type($(guard, env, init : void?) : int {})
+ var run_once = declare_extern_fn("jit_lib_run_once", run_once_type)
+ var registered = LLVMBuildCall2(b, run_once_type, run_once,
+ array(LLVMBuildPointerCast(b, guard_global, void_ptr, ""),
+ LLVMBuildPointerCast(b, env_global, void_ptr, ""),
+ LLVMBuildPointerCast(b, runtime_init, void_ptr, "")), "registered")
+ let ready_bb = LLVMAppendBasicBlockInContext(ctx, made.fn, "ready")
+ let declined_bb = LLVMAppendBasicBlockInContext(ctx, made.fn, "declined")
+ var can_run = LLVMBuildICmp(b, LLVMIntPredicate.LLVMIntNE, registered, types.ConstI32(0ul), "can_run")
+ LLVMBuildCondBr(b, can_run, ready_bb, declined_bb)
+ LLVMPositionBuilderAtEnd(b, declined_bb)
+ LLVMBuildRet(b, LLVMConstNull(void_ptr))
+ LLVMPositionBuilderAtEnd(b, ready_bb)
+ var global_context = emit_create_standalone_ctx(b, program_context, prog, types, sf.any_pinvoke)
+ let guard_type = jit_fn_type($(ctx, tramp, frame : void?) : int {})
+ var guard = declare_extern_fn("jit_lib_invoke_guarded", guard_type)
+ var ok = LLVMBuildCall2(b, guard_type, guard,
+ array(global_context, LLVMBuildPointerCast(b, ctx_init.fn, void_ptr, ""), LLVMConstNull(void_ptr)), "ok")
+ let finish_type = jit_fn_type($(ctx : void?; ok : int; err : void?) : void? {})
+ var finish = declare_extern_fn("jit_lib_create_finish", finish_type)
+ LLVMBuildRet(b, LLVMBuildCall2(b, finish_type, finish,
+ array(global_context, ok, LLVMBuildPointerCast(b, error_global, void_ptr, "")), ""))
+
+ var ctx_param = LLVMGetParam(ctx_init.fn, 0u)
+ let whole = emit_standalone_context_init(ctx_init.builder, runtime_builder, program_context, ctx,
+ prog, mod, types, uids, sf.funcs, ctx_param, register_all_modules, ships_dynamic,
+ sf.used_modules, dynamic_modules)
+ LLVMBuildRetVoid(ctx_init.builder)
+ return (fn = made.fn, link_whole_lib = whole)
+}
+
+
+[arch(at="../ARCHITECTURE_LIB.md#lib-runtime-scope")]
+def public inject_lib(program_context : Context?; ctx : LLVMContextRef; prog : Program?;
+ mod : LLVMOpaqueModule?; var types : PrimitiveTypes?; var uids : UidNodes?;
+ strict : bool; export_all : bool; output_path : string;
+ register_all_modules : bool = false) : tuple {
+ g_lib_registration_once = true
+ defer() {
+ g_lib_registration_once = false
+ }
+ g_lib_exports |> clear()
+ let prefix = lib_prefix_from_path(output_path)
+ if (prefix |> empty()) {
+ to_log(LOG_ERROR, "LLVM LIB: cannot derive a C symbol prefix from the output path `{output_path}` - name it after a C identifier\n")
+ return (fn = null, link_whole_lib = false)
+ }
+ let names = CNames(prefix = prefix, this_module = prog.getThisModule)
+ var selected <- collect_c_exports(prog, names, export_all)
+ for (r in selected.errors) {
+ failed(r.message)
+ }
+ if (!(selected.errors |> empty())) {
+ return (fn = null, link_whole_lib = false)
+ }
+ var exports <- selected.exports
+ if (exports |> empty()) {
+ to_log(LOG_ERROR, "LLVM LIB: nothing to export - annotate functions with [export_c], or pass -lib-export-all\n")
+ return (fn = null, link_whole_lib = false)
+ }
+ var sf <- collect_standalone_functions(prog, strict)
+ if (!sf.ok) {
+ return (fn = null, link_whole_lib = false)
+ }
+ var dynamic_modules : table
+ let rt = emit_runtime_init(ctx, prog, types, register_all_modules, sf.used_modules, dynamic_modules)
+ let created = emit_create(ctx, program_context, prog, mod, types, uids, prefix, sf, rt.fn,
+ rt.builder, register_all_modules, rt.ships_dynamic, dynamic_modules)
+ LLVMBuildRetVoid(rt.builder)
+ LLVMDisposeBuilder(rt.builder)
+ emit_fixed_entries(ctx, types, uids, sf.funcs, prefix,
+ LLVMGetNamedGlobal(g_mod, LIB_GUARD_GLOBAL), LLVMGetNamedGlobal(g_mod, LIB_ERROR_GLOBAL))
+ var emitted : array
+ for (e in exports) {
+ let thunk = emit_thunk(ctx, e, uids, types, export_all)
+ if (thunk |> is_none()) {
+ return (fn = null, link_whole_lib = false)
+ }
+ if (!(thunk |> unwrap())) {
+ emitted |> push_clone(e)
+ }
+ }
+ to_log(LOG_INFO, "LLVM LIB: C entry points generated: {prefix}_create/destroy/last_error plus {length(emitted)} exports, {length(sf.funcs)} functions\n")
+ g_lib_exports |> clear()
+ g_lib_exports |> push_clone_from(emitted)
+ return (fn = created.fn, link_whole_lib = created.link_whole_lib)
+}
diff --git a/modules/dasLLVM/daslib/llvm_jit.das b/modules/dasLLVM/daslib/llvm_jit.das
index 38cf6f943b..5a5a8c7e90 100644
--- a/modules/dasLLVM/daslib/llvm_jit.das
+++ b/modules/dasLLVM/daslib/llvm_jit.das
@@ -91,7 +91,7 @@ def set_debug_linkage(value : LLVMOpaqueValue?) {
LLVMSetLinkage(value, LLVMLinkage.LLVMInternalLinkage)
}
-def set_public_linkage(value : LLVMOpaqueValue?) {
+def public set_public_linkage(value : LLVMOpaqueValue?) {
LLVMSetLinkage(value, LLVMLinkage.LLVMDLLExportLinkage)
LLVMSetDLLStorageClass(value, LLVMDLLStorageClass.LLVMDLLExportStorageClass)
}
@@ -8156,5 +8156,7 @@ def init_llvm_jit_module_options() {
this_module() |> add_module_option("jit_target", Type.tString)
this_module() |> add_module_option("jit_split_modules", Type.tInt)
this_module() |> add_module_option("jit_obj_cache", Type.tBool)
+ this_module() |> add_module_option("jit_lib", Type.tBool)
+ this_module() |> add_module_option("jit_lib_export_marked", Type.tBool)
}
}
diff --git a/modules/dasLLVM/daslib/llvm_jit_cli.das b/modules/dasLLVM/daslib/llvm_jit_cli.das
index 35b7eaf975..cbcef25d2d 100644
--- a/modules/dasLLVM/daslib/llvm_jit_cli.das
+++ b/modules/dasLLVM/daslib/llvm_jit_cli.das
@@ -74,6 +74,22 @@ struct public JitCliOptions {
@clarg_doc = "JIT: explicit linker binary (overrides default c++/clang/lld-link). Windows-MSVC builds a link.exe-flavored cmd (/DLL /OUT:) — pass a link.exe-compatible linker; elsewhere match the compiler daslang was built with to avoid sanitizer runtime mismatch."
path_to_linker : Option
+ @clarg_name = "jit-lib"
+ @clarg_doc = "JIT: emit a C-ABI native library plus its C header instead of running the program. Script-level pin: options jit_lib = true"
+ lib : Option
+
+ @clarg_name = "jit-lib-export-marked"
+ @clarg_doc = "JIT (-lib): export every function the program already marks [export], not only the [export_c] ones. Selection only - unlike -lib-export-all it marks nothing itself, so a non-representable signature is still a hard error. Script-level pin: options jit_lib_export_marked = true"
+ lib_export_marked : Option
+
+ @clarg_name = "jit-lib-static"
+ @clarg_doc = "JIT (-lib): produce a static archive (.a / .lib) instead of a shared library; the host then links libDaScript_runtime itself (the generated header records what). Under this flag --jit-path-to-linker names the ARCHIVER (llvm-ar / ar / lib), not a linker"
+ lib_static : Option
+
+ @clarg_name = "jit-lib-bindings"
+ @clarg_doc = "JIT (-lib): also write daslang `[extern]` bindings for the emitted library to this path, so a daslang host requires them instead of hand-writing the declarations or parsing the C header"
+ lib_bindings : Option
+
@clarg_name = "jit-register-all-modules"
@clarg_doc = "JIT (-exe only): register all builtin native modules (math, fio, dasbind, ...) at exe startup, so a standalone compiler-driver exe can recompile arbitrary daslang at runtime"
register_all_modules : Option
diff --git a/modules/dasLLVM/daslib/llvm_jit_common.das b/modules/dasLLVM/daslib/llvm_jit_common.das
index ad97f866aa..7aa4a31383 100644
--- a/modules/dasLLVM/daslib/llvm_jit_common.das
+++ b/modules/dasLLVM/daslib/llvm_jit_common.das
@@ -537,6 +537,15 @@ def public jit_extern_type(t : Type) : LLVMTypeRef {
return t == Type.tVoid ? g_prim_t.t_void : base_type_to_llvm_type(t)
}
+def public declare_extern_fn(name : string; typ : LLVMTypeRef) : LLVMOpaqueValue? {
+ var fn = LLVMGetNamedFunction(g_mod, name)
+ if (fn == null) {
+ fn = LLVMAddFunctionWithType(g_mod, name, typ)
+ }
+ return fn
+}
+
+
def public jit_add_extern(name : string; typ : LLVMTypeRef; fnAddr : void?; attrs : LLVMOpaqueAttributeRef? []) {
var f = LLVMAddFunctionWithType(g_mod, name, typ)
LLVMAddGlobalMapping(g_engine, f, fnAddr)
@@ -1011,7 +1020,8 @@ def public with_default_target_machine(opt_level : uint; use_host_cpu : bool;
// Offline AOT: emit a native object only, no linker step (jit_emit_object path).
// use_host_cpu=false (default) targets a generic CPU so the .o is portable; true bakes host features.
-def public emit_object_only(mod : LLVMOpaqueModule?; out_path : string; use_host_cpu : bool = false) {
+def public emit_object_only(mod : LLVMOpaqueModule?; out_path : string; use_host_cpu : bool = false) : bool {
+ var ok = true
// Codegen level 3 deliberate: shipped artifacts, no cache guard (ARCHITECTURE.md 1.2)
with_default_target_machine(3u, use_host_cpu) $(targetMachine : LLVMTargetMachineRef) {
// A module with no triple and no layout leaves the object to whatever the backend
@@ -1021,13 +1031,15 @@ def public emit_object_only(mod : LLVMOpaqueModule?; out_path : string; use_host
LLVMSetTarget(mod, LLVMGetDefaultTargetTriple())
LLVMDisposeTargetData(dl)
let error : string?
- let file = add_obj_extension(out_path)
+ let file = artifact_path(out_path, JitArtifact.object)
let filetype = LLVMCodeGenFileType.LLVMObjectFile
let failed = LLVMTargetMachineEmitToFile(targetMachine, mod, file, filetype, error)
if (failed != 0) {
- panic("emit_object_only: LLVMTargetMachineEmitToFile failed for {file}")
+ to_log(LOG_ERROR, "emit_object_only: LLVMTargetMachineEmitToFile failed for {file}\n")
+ ok = false
}
}
+ return ok
}
// nolint:STYLE014
@@ -1059,7 +1071,7 @@ def public link_dll_from_objects(objs : array; out_path : string; path_t
return false
}
let phase_tm = ref_time_ticks()
- let ok = create_shared_library(objs[0], add_dll_extension(out_path), "{path_to_dascript_lib}", "{path_to_linker}", "\"@{rsp}\" {linker_string}", true, link_whole_lib, debug_info)
+ let ok = create_shared_library(objs[0], artifact_path(out_path, JitArtifact.jit_dll), "{path_to_dascript_lib}", "{path_to_linker}", "\"@{rsp}\" {linker_string}", true, link_whole_lib, debug_info)
if (log_time) {
to_log(LOG_INFO, "LLVM JIT time: link {jit_sec(get_time_usec(phase_tm))} ({length(objs)} objects)\n")
}
@@ -1069,44 +1081,41 @@ def public link_dll_from_objects(objs : array; out_path : string; path_t
// @out_path - folder + file name, without extension. @path_to_dascript_lib - required on Windows,
// no effect on Linux. @path_to_linker - linker override; Windows-MSVC defaults to lld-link (link.exe
// flavor) from the LLVM package, c++/clang elsewhere.
-def public write_dll(mod : LLVMOpaqueModule?; out_path : string; path_to_dascript_lib, path_to_linker, linker_string : string; link_whole_lib : bool; debug_info : bool = false; log_time : bool = false; codegen_opt_level : uint = 3u) {
- // JIT DLL cache: emitted artifact only ever runs on this host.
- with_default_target_machine(codegen_opt_level, true) $(targetMachine : LLVMTargetMachineRef) {
+def public write_artifact(mod : LLVMOpaqueModule?; out_path : string; kind : JitArtifact;
+ path_to_dascript_lib, path_to_linker, linker_string : string;
+ link_whole_lib : bool; use_host_cpu : bool; debug_info : bool = false;
+ log_time : bool = false; codegen_opt_level : uint = 3u) : bool {
+ var emitted = true
+ with_default_target_machine(codegen_opt_level, kind == JitArtifact.jit_dll ? true : use_host_cpu) $(targetMachine : LLVMTargetMachineRef) {
let error : string?
- let file = add_obj_extension(out_path)
- let filetype = LLVMCodeGenFileType.LLVMObjectFile
+ let file = artifact_path(out_path, JitArtifact.object)
var phase_tm = ref_time_ticks()
- let failed = LLVMTargetMachineEmitToFile(targetMachine, mod, file, filetype, error)
- if (failed != 0) {
- panic("write_dll: LLVMTargetMachineEmitToFile failed for {file}")
+ if (LLVMTargetMachineEmitToFile(targetMachine, mod, file, LLVMCodeGenFileType.LLVMObjectFile, error) != 0) {
+ to_log(LOG_ERROR, "write_artifact: LLVMTargetMachineEmitToFile failed for {file}\n")
+ emitted = false
+ return
}
let t_emit = get_time_usec(phase_tm)
phase_tm = ref_time_ticks()
- let ok = create_shared_library(file, add_dll_extension(out_path), "{path_to_dascript_lib}", "{path_to_linker}", "{linker_string}", true, link_whole_lib, debug_info)
+ let target = artifact_path(out_path, kind)
+ var ok = false
+ if (kind == JitArtifact.static_lib) {
+ ok = create_static_library(file, target, "{path_to_linker}")
+ } else {
+ ok = create_shared_library(file, target, "{path_to_dascript_lib}", "{path_to_linker}",
+ "{linker_string}", kind != JitArtifact.exe, link_whole_lib, debug_info)
+ }
if (log_time) {
to_log(LOG_INFO, "LLVM JIT time: emit-obj {jit_sec(t_emit)} link {jit_sec(get_time_usec(phase_tm))}\n")
}
if (!ok) {
- panic("write_dll: link failed for {out_path}")
+ to_log(LOG_ERROR, "write_artifact: {kind == JitArtifact.static_lib ? "archive" : "link"} failed for {target}\n")
+ emitted = false
}
}
+ return emitted
}
-def public write_exe(mod : LLVMOpaqueModule?; out_path : string; path_to_dascript_lib, path_to_linker, linker_string : string; link_whole_lib : bool; use_host_cpu : bool; debug_info : bool = false) {
- // Standalone exe: generic (redistributable) by default; use_host_cpu targets the box so
- // tuner-generated host-specific IR legalizes (the generic target aborts codegen on it).
- // Codegen level 3 deliberate: shipped artifacts, no cache guard (ARCHITECTURE.md 1.2)
- with_default_target_machine(3u, use_host_cpu) $(targetMachine : LLVMTargetMachineRef) {
- let error : string?
- let file = add_obj_extension(out_path)
- let filetype = LLVMCodeGenFileType.LLVMObjectFile
- LLVMTargetMachineEmitToFile(targetMachine, mod, file, filetype, error)
- let ok = create_shared_library(file, add_exe_extension(out_path), "{path_to_dascript_lib}", "{path_to_linker}", "{linker_string}", false, link_whole_lib, debug_info)
- if (!ok) {
- panic("write_exe: link failed for {out_path}")
- }
- }
-}
// Locate the wasm64 (memory64) libDaScript_runtime.a. Non-empty `override_path` wins (e.g.
// --jit-runtime-lib CLI flag; a missing file logs a warning); otherwise auto-locate at
@@ -1124,7 +1133,8 @@ def private find_runtime_lib(override_path : string) : Option {
// Cross-compile a module to wasm64 (memory64) and link a runnable .wasm via emcc against the
// web/output64 runtime archive (-sMEMORY64=1). triple defaults to wasm64-unknown-emscripten;
// needs_runtime gates linking libDaScript_runtime.a (true=link if found else warn; false=omit).
-def public write_wasm(mod : LLVMOpaqueModule?; out_path : string; triple : string; path_to_emcc : string; needs_runtime : bool; explicit_runtime : string = ""; emit_object_only : bool = false) {
+def public write_wasm(mod : LLVMOpaqueModule?; out_path : string; triple : string; path_to_emcc : string; needs_runtime : bool; explicit_runtime : string = ""; emit_object_only : bool = false) : bool {
+ var wasm_ok = true
LLVMInitializeWasmTarget()
let real_triple = (!(triple |> empty()) ? triple : "wasm64-unknown-emscripten")
let runtime_lib = find_runtime_lib(explicit_runtime)
@@ -1147,14 +1157,16 @@ def public write_wasm(mod : LLVMOpaqueModule?; out_path : string; triple : strin
let error : string?
// emit_object_only: write the object verbatim to out_path (no `.o.o`
// double extension) so the caller has a predictable, linkable artifact.
- let obj = emit_object_only ? out_path : add_obj_extension(out_path)
+ let obj = emit_object_only ? out_path : artifact_path(out_path, JitArtifact.object)
let filetype = LLVMCodeGenFileType.LLVMObjectFile
LLVMTargetMachineEmitToFile(targetMachine, mod, obj, filetype, error)
if (emit_object_only) {
// The caller (e.g. daspkg release wasm) runs emcc; verify the object
// exists so a failed emit doesn't silently hand off a missing file.
if (error != null || !stat(obj).is_valid) {
- panic("write_wasm: failed to emit object {obj} (LLVMTargetMachineEmitToFile error)")
+ to_log(LOG_ERROR, "write_wasm: failed to emit object {obj} (LLVMTargetMachineEmitToFile error)\n")
+ wasm_ok = false
+ return
}
to_log(LOG_INFO, "wasm: emitted object {obj} (link skipped; --jit-emit-object)\n")
return
@@ -1162,9 +1174,11 @@ def public write_wasm(mod : LLVMOpaqueModule?; out_path : string; triple : strin
let runtime_arg = needs_runtime ? (runtime_lib ?? "") : ""
let ok = link_wasm(obj, "{out_path}.wasm", runtime_arg, path_to_emcc, true)
if (!ok) {
- panic("write_wasm: link failed for {out_path}.wasm")
+ to_log(LOG_ERROR, "write_wasm: link failed for {out_path}.wasm\n")
+ wasm_ok = false
}
}
+ return wasm_ok
}
def public build_string_constant(message : string) {
diff --git a/modules/dasLLVM/daslib/llvm_jit_link.das b/modules/dasLLVM/daslib/llvm_jit_link.das
index af7994fc96..9ff1aeea65 100644
--- a/modules/dasLLVM/daslib/llvm_jit_link.das
+++ b/modules/dasLLVM/daslib/llvm_jit_link.das
@@ -67,7 +67,7 @@ def public run_jit_linked(prog : Program?; var ctx : Context?) : bool {
if (!plan.use_dll) {
return run_jit_in_emitter(prog, ctx, plan.log_jit_time, t_hash, 0)
}
- if (empty(plan.candidates)) {
+ if (empty(plan.candidates) && !plan.gen_lib) {
to_log(LOG_INFO, "LLVM JIT: 0 functions to jit ({length(plan.disabled)} marked no_jit) - the WHOLE program runs interpreted\n")
return true
}
diff --git a/modules/dasLLVM/daslib/llvm_jit_plan.das b/modules/dasLLVM/daslib/llvm_jit_plan.das
index b52fba518d..69474bad1e 100644
--- a/modules/dasLLVM/daslib/llvm_jit_plan.das
+++ b/modules/dasLLVM/daslib/llvm_jit_plan.das
@@ -719,6 +719,10 @@ struct public JitPlan {
debug_info : bool
dump_ir : bool
gen_exe : bool
+ gen_lib : bool
+ lib_static : bool
+ lib_export_marked : bool
+ lib_bindings : string
exe_main : string
compile_only : bool
emit_aot_object : bool
@@ -774,7 +778,19 @@ def public make_jit_plan(prog : Program?; var ctx : Context?; announce : bool) :
to_log(LOG_WARNING, "LLVM JIT: dll mode requested but this daslang build is static - no DLL cache, " +
"every run pays full in-memory codegen\n")
}
- plan.gen_exe = prog.policies.jit_exe_mode
+ plan.gen_lib = cli_opts.lib |> unwrap_or((prog._options |> find_arg("jit_lib")) ?as tBool ?? false)
+ plan.gen_exe = prog.policies.jit_exe_mode || plan.gen_lib
+ plan.lib_static = cli_opts.lib_static |> unwrap_or(false)
+ plan.lib_bindings = cli_opts.lib_bindings |> unwrap_or("")
+ plan.lib_export_marked = cli_opts.lib_export_marked |> unwrap_or((prog._options |> find_arg("jit_lib_export_marked")) ?as tBool ?? false)
+ if (announce && !plan.gen_lib) {
+ if (cli_opts.lib_static |> is_some()) {
+ to_log(LOG_WARNING, "LLVM JIT: --jit-lib-static applies to -lib only - ignored for this run\n")
+ }
+ if (cli_opts.lib_bindings |> is_some()) {
+ to_log(LOG_WARNING, "LLVM JIT: --jit-lib-bindings applies to -lib only - ignored for this run\n")
+ }
+ }
plan.exe_main = "main"
// --jit-compile-only: build + verify + optimize (+dump), write/load/install nothing —
// the program runs interpreted. Also bypasses the DLL cache probe, so the
diff --git a/modules/dasLLVM/daslib/llvm_jit_run.das b/modules/dasLLVM/daslib/llvm_jit_run.das
index 5e32460b1c..942e16db08 100644
--- a/modules/dasLLVM/daslib/llvm_jit_run.das
+++ b/modules/dasLLVM/daslib/llvm_jit_run.das
@@ -8,6 +8,8 @@ require llvm/daslib/llvm_boost
require llvm/daslib/llvm_dll_utils
require llvm/daslib/llvm_jit_plan public
require llvm/daslib/llvm_exe
+require llvm/daslib/jit_standalone
+require daslib/c_api_header
require llvm/daslib/llvm_aot
require llvm/daslib/llvm_jit
require llvm/daslib/llvm_jit_common
@@ -35,7 +37,7 @@ var LINK_WHOLE_LIB = false // when true, standalone exe links against the whole
// Read by tests-cpp/small/test_jit_emitter_pin.cpp: FNV-1a64 of the emitter sources
// (normalized to LF; file list in the test)
-let LLVM_JIT_EMITTER_HASH : uint64 = 0xa2ab0760ed831369ul
+let LLVM_JIT_EMITTER_HASH : uint64 = 0x6278c66628ebdb61ul
def private apply_fast_math_to_module(m : LLVMOpaqueModule?) {
var fn = LLVMGetFirstFunction(m)
@@ -310,8 +312,8 @@ def private run_split_codegen(prog : Program?; ctx : Context?; funcs : array push(obj) // link order = partition order, hit or miss
if (obj_cache && stat(obj).is_valid) {
cached++
@@ -448,6 +450,9 @@ def public run_jit(prog : Program?; var ctx : Context?) : bool { // nolint:STYL
assume debug_info = plan.debug_info
assume dump_ir = plan.dump_ir
assume gen_exe = plan.gen_exe
+ assume gen_lib = plan.gen_lib
+ assume lib_static = plan.lib_static
+ assume lib_bindings = plan.lib_bindings
assume exe_main = plan.exe_main
assume compile_only = plan.compile_only
assume emit_aot_object = plan.emit_aot_object
@@ -493,6 +498,8 @@ def public run_jit(prog : Program?; var ctx : Context?) : bool { // nolint:STYL
init_jit_target_flags(target_triple, use_host_cpu)
var funcs : array
var visitorDisabled = 0
+ var emit_failed = false
+ defer() { reset_codegen_accumulators(); }
var disableJitVisitor = new DisableJitVisitor()
make_visitor(*disableJitVisitor) $(disableJitVisitorAdapter) {
for (fun in plan.candidates) {
@@ -515,7 +522,7 @@ def public run_jit(prog : Program?; var ctx : Context?) : bool { // nolint:STYL
// emit_aot_object always writes an object even with no functions to emit (a fully-no_aot
// module): an empty .o with just the fileinfo ctor, so the build always has the file it
// expects and those functions interpret at load (as Program::linkCppAot skips them).
- if (!empty(funcs) || emit_aot_object) {
+ if (!empty(funcs) || emit_aot_object || gen_lib) {
// the plan's fold over the candidates - the first aot-hash per function lands there (cached after)
let t_hash = get_time_usec(phase_tm)
phase_tm = ref_time_ticks()
@@ -600,7 +607,18 @@ def public run_jit(prog : Program?; var ctx : Context?) : bool { // nolint:STYL
reopen_and_gc_dll(g_dynamic_lib_handle, output_path, funcs, disabled, uids, lto_probe ? "" : dll_hash_basename, sres.objs)
} elif (recompile_prog) {
jit_has_externals = irgen_functions(ctx, uids, attrs, funcs, jit_flags, jit_mode, prog)
- if (gen_exe) {
+ if (gen_lib) {
+ let export_marked = plan.lib_export_marked
+ let res = inject_lib(ctx, g_ctx, prog, g_mod, g_prim_t, uids, exe_strict,
+ prog.policies.export_public_functions || export_marked, output_path,
+ plan.register_all_modules)
+ LINK_WHOLE_LIB = res.link_whole_lib
+ if (res.fn == null) {
+ finalize_jit(use_dll, gen_exe, g_dynamic_lib_handle)
+ to_log(LOG_ERROR, "LLVM LIB: no library written for {output_path}\n")
+ return false
+ }
+ } elif (gen_exe) {
let res = inject_main(ctx, g_ctx, prog, exe_main, g_mod, g_prim_t, uids, exe_strict,
plan.register_all_modules)
LINK_WHOLE_LIB = res.link_whole_lib
@@ -645,19 +663,36 @@ def public run_jit(prog : Program?; var ctx : Context?) : bool { // nolint:STYL
if (compile_only) {
to_log(LOG_INFO, "LLVM JIT: compile-only - module built, optimized and verified; no artifact written, nothing installed\n")
} elif (emit_aot_object) {
- emit_object_only(g_mod, output_path, use_host_cpu)
+ emit_failed ||= !emit_object_only(g_mod, output_path, use_host_cpu)
+ } elif (gen_lib) {
+ mkdir_rec(dir_name(output_path))
+ emit_failed ||= !write_artifact(g_mod, output_path, lib_static ? JitArtifact.static_lib : JitArtifact.shared_lib,
+ path_to_shared_lib, path_to_linker, linker_string, LINK_WHOLE_LIB, exe_host_cpu,
+ debug_info, log_jit_time)
+ let lib_names = CNames(prefix = lib_prefix_from_path(output_path), this_module = prog.getThisModule)
+ if (!emit_c_header(g_lib_exports, lib_names, string(prog.getThisModule.fileName),
+ output_path, lib_link_note(lib_static, LINK_WHOLE_LIB))) {
+ to_log(LOG_ERROR, "LLVM LIB: the library is written but its C header is not - {output_path}.h\n")
+ emit_failed = true
+ }
+ if (!(lib_bindings |> empty())) {
+ emit_failed ||= !write_das_bindings(g_lib_exports, lib_names, string(prog.getThisModule.fileName),
+ output_path, lib_bindings, lib_static)
+ }
} elif (gen_wasm) {
mkdir_rec(dir_name(output_path))
// path_to_linker is reused as the optional emcc override
// for the wasm link (mirrors how it's used for the host
// linker, lld-link/clang/c++, on the host path).
- write_wasm(g_mod, output_path, target_triple, path_to_linker, jit_has_externals, runtime_lib_override, emit_object)
+ emit_failed ||= !write_wasm(g_mod, output_path, target_triple, path_to_linker, jit_has_externals, runtime_lib_override, emit_object)
} elif (gen_exe) {
mkdir_rec(dir_name(output_path))
- write_exe(g_mod, output_path, path_to_shared_lib, path_to_linker, linker_string, LINK_WHOLE_LIB, exe_host_cpu, debug_info)
+ emit_failed ||= !write_artifact(g_mod, output_path, JitArtifact.exe, path_to_shared_lib, path_to_linker,
+ linker_string, LINK_WHOLE_LIB, exe_host_cpu, debug_info)
} elif (use_dll) {
mkdir_rec(dir_name(output_path))
- write_dll(g_mod, output_path, path_to_shared_lib, path_to_linker, linker_string, LINK_WHOLE_LIB, debug_info, log_jit_time, opt_level |> uint)
+ emit_failed ||= !write_artifact(g_mod, output_path, JitArtifact.jit_dll, path_to_shared_lib, path_to_linker,
+ linker_string, LINK_WHOLE_LIB, true, debug_info, log_jit_time, opt_level |> uint)
let no_keep_objs : array // monolith artifacts all carry the dll-hash prefix
reopen_and_gc_dll(g_dynamic_lib_handle, output_path, funcs, disabled, uids, dll_hash_basename, no_keep_objs)
}
@@ -678,7 +713,7 @@ def public run_jit(prog : Program?; var ctx : Context?) : bool { // nolint:STYL
}
jit_log_report(funcs, true, opt_level, true, false, 0, debug_info, log_jit_time, totalTime,
t_hash, t_init, t_declare, t_probe, t_irgen, t_opt, t_emit, 0, 0)
- return true
+ return !emit_failed
}
if (use_dll) {
assert(g_dynamic_lib_handle.handle != null)
@@ -721,5 +756,5 @@ def public run_jit(prog : Program?; var ctx : Context?) : bool { // nolint:STYL
} else {
to_log(LOG_INFO, "LLVM JIT: 0 functions to jit ({length(disabled)} marked no_jit, {visitorDisabled} disabled by content) - the WHOLE program runs interpreted\n")
}
- return true
+ return !emit_failed
}
diff --git a/skills/cpp_integration.md b/skills/cpp_integration.md
index 6d12d75e9c..13b1d20bee 100644
--- a/skills/cpp_integration.md
+++ b/skills/cpp_integration.md
@@ -297,7 +297,7 @@ and link the result against nano instead:
#include "script.das.h"
das::das_nano_set_print(&my_uart_write); // every print leaves through this
-script::Standalone ctx; // a plain C++ object
+ctx_script::Standalone ctx; // a plain C++ object
int answer = ctx.exported_function(21);
```
@@ -346,6 +346,56 @@ link. Worked example: `examples/standalone/06_full_runtime/` - read it for the s
it needs the daslang repository, since the bundle carries no dasHV headers or archive. The recipe
above works from a bundle for any C++ module you build yourself.
+## Calling daslang from C - `daslang -lib`
+
+When the host is C, or wants no daslang API at all, compile the script to a native library with a
+generated C header instead:
+
+```sh
+daslang -lib script.das -output build/script # build/script.so (.dylib/.dll) + build/script.h
+daslang -lib script.das -output build/script -- --jit-lib-static # build/script.a instead
+```
+
+Three ways to pick what crosses: mark each function `[export_c]` (an `[export]` the library also
+surfaces in C); pass `-- --jit-lib-export-marked` to take whatever the program already marks
+`[export]`; or pass `-lib-export-all` for every public function of the entry module whose signature
+C can spell - only export-all skips an unspellable one with a warning, the other two make it a hard
+error. `-lib` carries the annotation itself; add `require daslib/export_c` to compile that same
+source without the JIT, which the linter and the AOT pass both do. `examples/c_api_library/` builds
+one library each way and binds all three at once.
+
+A daslang host needs no C at all: `-- --jit-lib-bindings out/script_c.das` writes the daslang twin
+of the header - one `[extern(cdecl, late, ...)]` per entry point plus a das struct per structure
+that crosses - and the host `require`s that file. The externs are `late`, so the bindings compile
+before the library exists and a build system can generate them as an ordinary output.
+
+For a C host, then:
+
+```c
+#include "script.h"
+
+script_ctx * ctx = script_create(); /* one instance = one context, globals and heap */
+script_Vec3 v = { 1.0f, 2.0f, 3.0f }, out;
+script_scale(ctx, &v, 2.0f, &out); /* a struct or vector result uses a trailing out ptr */
+if ( script_last_error(ctx) ) { /* the call raised; out is untouched */ }
+script_destroy(ctx);
+```
+
+Scalars, `string` (as `const char *`), pointers and enums cross by value; everything else
+representable crosses as `const T *`. A daslang panic returns zero and reports through
+`script_last_error(ctx)` - it never unwinds into C. A returned `const char *` lives in that
+instance's string heap, so copy it if you need it past the next call. The header asserts the
+layout of every structure it declares, so a host built for a different target fails to compile.
+Several such libraries coexist in one process, as does a library inside a host that registered
+the daslang modules itself - the first one there registers the runtime and the rest bind to it.
+Several instances of one library are fine, created and driven on any thread.
+
+Choosing between the four: **nano** when the host is C++ and you want the smallest runtime; a
+**standalone context on the full runtime** when the host is C++ and the script reaches a C++
+module beyond `builtin`; **`-lib`** when the host is C, or wants a plain ABI boundary and no
+daslang headers; the **C API** (`daScriptC.h`) when the host has to compile daslang itself at run
+time.
+
## Diagnostics - `TextPrinter`, never `fprintf(stderr, ...)`
```cpp
diff --git a/skills/daslang/references/cli-and-config.md b/skills/daslang/references/cli-and-config.md
index 56e6ed1502..5b4b9d2253 100644
--- a/skills/daslang/references/cli-and-config.md
+++ b/skills/daslang/references/cli-and-config.md
@@ -83,6 +83,9 @@ interpreter wires it to `-?` instead:
help : bool
```
+A `-lib` library never owns argv at all: the host's process arguments are whatever the host was
+started with, so a library reads its configuration from its C parameters, not from `clargs`.
+
Only an `-exe` binary owns argv, so `-h` / `--help` - and `parse_args_with_help`'s automatic help
flag - are reachable only there.
diff --git a/skills/daslang/references/modules-and-stdlib.md b/skills/daslang/references/modules-and-stdlib.md
index 3f3d768f12..cf231e5a73 100644
--- a/skills/daslang/references/modules-and-stdlib.md
+++ b/skills/daslang/references/modules-and-stdlib.md
@@ -22,7 +22,10 @@ def main { print("ok\n") }
The only enforced ordering is `module` before any type declaration; `options` / `module` /
`require` otherwise interleave. A file with no `module` line is a program, named by its file stem.
-`[export]` makes a function callable from the host by name; `[init]` / `[finalize]` run at context
+`[export]` makes a function callable from the host by name, and `[export_c]` is an `[export]` that
+`daslang -lib` also surfaces as a C function in the header it generates (`-lib`/`-jit`/`-exe` carry
+the annotation; a non-JIT compile of that source needs `require daslib/export_c`) (`[export_c(name = "...")]`
+picks the C symbol, which is how two overloads both reach C); `[init]` / `[finalize]` run at context
init / shutdown (no arguments, no return). `main` is a convention, not a keyword: it returns `void`
unless declared `def main() : int`, whose return value is the process exit code (do not `panic` to
force one).
diff --git a/src/ast/ast_export.cpp b/src/ast/ast_export.cpp
index 27022f2a71..aa9801ab52 100644
--- a/src/ast/ast_export.cpp
+++ b/src/ast/ast_export.cpp
@@ -132,6 +132,14 @@ namespace das {
return true;
}, "*");
}
+ void exportPublicFunctions( Module * thisModule ) {
+ for ( auto & fn : thisModule->functions.each() ) {
+ if ( fn->privateFunction || fn->builtIn || fn->generated || fn->isTemplate ) continue;
+ if ( fn->macroInit || fn->macroFunction || fn->init || fn->shutdown ) continue;
+ if ( fn->isClassMethod || fn->lambda || fn->generator || fn->fromGeneric ) continue;
+ fn->exports = true;
+ }
+ }
void markModuleVarsUsed( ModuleLibrary &, Module * inWhichModule ) {
for ( auto & var : inWhichModule->globals.each() ) {
program->setUsed(var, false);
@@ -346,6 +354,7 @@ namespace das {
MarkSymbolUse vis(this, false);
vis.tw = logs;
visit(vis);
+ if ( policies.export_public_functions ) vis.exportPublicFunctions(thisModule.get());
vis.markUsedFunctions(library, false, false, nullptr);
vis.markVarsUsed(library, false);
}
diff --git a/src/ast/ast_module.cpp b/src/ast/ast_module.cpp
index 1a4ab1ffb8..7702b7cb49 100644
--- a/src/ast/ast_module.cpp
+++ b/src/ast/ast_module.cpp
@@ -127,9 +127,6 @@ namespace das {
atomic g_envTotal(0);
- // from module_builtin_fio.cpp — modules whose .shared_module dlopen failed (Quiet)
- DAS_API string describe_pending_dynamic_modules();
-
static void daslang_atexit_audit() {
int n = g_envTotal.load();
if ( n != 0 ) {
diff --git a/src/ast/ast_parse.cpp b/src/ast/ast_parse.cpp
index 1a4cbd45a4..1207bb941b 100644
--- a/src/ast/ast_parse.cpp
+++ b/src/ast/ast_parse.cpp
@@ -1663,9 +1663,6 @@ namespace das {
}
}
- // from module_builtin_fio.cpp — modules whose .shared_module dlopen failed (Quiet)
- DAS_API string describe_pending_dynamic_modules();
-
ProgramPtr reportPrerequisitesErrors (
const string & fileName,
const vector & missing,
diff --git a/src/ast/ast_print.cpp b/src/ast/ast_print.cpp
index dafe7966ec..98d06b2c1b 100644
--- a/src/ast/ast_print.cpp
+++ b/src/ast/ast_print.cpp
@@ -1471,7 +1471,7 @@ namespace das {
void Program::setPrintFlags() {
#if defined(STANDALONE_MODE)
- ast_print::Standalone ctx;
+ ctx_ast_print::Standalone ctx;
ctx.setFlags(this);
#else
ClearPrinterFlags cflags;
diff --git a/src/builtin/jit_runtime.cpp b/src/builtin/jit_runtime.cpp
index 1f131cc534..ba5fa7cd7d 100644
--- a/src/builtin/jit_runtime.cpp
+++ b/src/builtin/jit_runtime.cpp
@@ -493,7 +493,9 @@ extern "C" {
if ( !jit_module_is_registered(moduleName) ) {
DAS_FATAL_ERROR("Failed to find %s: module %s is not registered (its .shared_module may have failed to load - see errors above).\n", funcMangledName, moduleName);
}
- DAS_FATAL_ERROR("Failed to find %s in module %s.\n", funcMangledName, moduleName);
+ das::string pending = describe_pending_dynamic_modules();
+ DAS_FATAL_ERROR("Failed to find %s in module %s.%s%s\n", funcMangledName, moduleName,
+ pending.empty() ? "" : " Dynamic modules still pending: ", pending.c_str());
}
}
@@ -1189,12 +1191,73 @@ DAS_API void das_ensure_environment () {
das::daScriptEnvironment::ensure();
}
+DAS_API void * jit_register_module_once ( const char * dasName, das::Module * (*reg)() ) {
+ das::daScriptEnvironment::ensure();
+ if ( das::Module * have = das::Module::require(dasName ? dasName : "") ) return have;
+ return reg();
+}
+
+DAS_API int32_t jit_lib_run_once ( int32_t * guard, void ** env, void (*fn)() ) {
+ static das::mutex once_mutex;
+ das::lock_guard lock(once_mutex);
+ if ( *guard ) {
+ if ( *env && das::daScriptEnvironment::getBound()!=*env ) {
+ das::daScriptEnvironment::setBound((das::daScriptEnvironment *)*env);
+ }
+ return 1;
+ }
+ das::daScriptEnvironment::ensure();
+ *env = das::daScriptEnvironment::getBound();
+ *guard = das::daScriptEnvironment::getBound()->modules ? 2 : 1;
+ fn();
+ return 1;
+}
+
+DAS_API int32_t jit_lib_invoke_guarded ( das::Context * ctx, void (*tramp)(das::Context *, void *), void * frame ) {
+ if ( !ctx ) return 0;
+ ctx->clearException();
+ if ( ctx->contextMutex ) {
+ das::lock_guard guard(*ctx->contextMutex);
+ return ctx->runWithCatch([&]() { tramp(ctx, frame); }) ? 1 : 0;
+ }
+ return ctx->runWithCatch([&]() { tramp(ctx, frame); }) ? 1 : 0;
+}
+
+DAS_API das::Context * jit_lib_create_finish ( das::Context * ctx, int32_t ok, char ** err ) {
+ if ( ok ) return ctx;
+ const char * why = ctx ? ( ctx->getException() ? ctx->getException() : "unknown exception" )
+ : "out of memory";
+ if ( err ) {
+ free(*err);
+ const size_t len = strlen(why) + 1;
+ *err = (char *) malloc(len);
+ if ( *err ) memcpy(*err, why, len);
+ }
+ delete ctx;
+ return nullptr;
+}
+
+DAS_API const char * jit_lib_last_error ( das::Context * ctx ) {
+ return ctx ? ctx->getException() : nullptr;
+}
+
+DAS_API void jit_destroy_standalone_ctx ( das::Context * ctx ) {
+ delete ctx;
+}
+
DAS_API void jit_initialize_modules () {
// No need to initialize modules. JIT will generate required calls.
das::daScriptEnvironment::ensure();
}
-DAS_API void jit_initialize_modules_done () {
+DAS_API void jit_initialize_modules_done ( int32_t guard ) {
+ if ( guard==2 ) {
+ das::string notInitialized;
+ if ( !das::Module::InitializeDependencies(notInitialized) ) {
+ das::LOG(das::LogLevel::error) << "LLVM LIB: unable to initialize modules:" << notInitialized << "\n";
+ }
+ return;
+ }
das::Module::Initialize();
}
@@ -1206,6 +1269,11 @@ DAS_API void jit_shutdown () {
das::Module::ShutdownStandalone();
}
+DAS_API void jit_lib_shutdown ( int32_t guard ) {
+ if ( guard!=1 ) return;
+ das::Module::ShutdownStandalone();
+}
+
DAS_API void * jit_register_dynamic_module ( const char * path, const char * mod_name ) {
return das::register_dynamic_module(path, mod_name, 0/*Quiet*/, nullptr, nullptr);
}
@@ -1263,6 +1331,11 @@ DAS_API void jit_finalize_dynamic_modules () {
}
}
+DAS_API void jit_lib_finalize_dynamic_modules () {
+ das::retry_pending_dynamic_modules();
+ das::report_pending_dynamic_modules();
+}
+
// ABI shim: -exe binaries emitted before the resolving form link this runtime dynamically
// and still import the 3-argument name.
DAS_API void jit_register_native_path ( const char * mod_name, const char * src_path, const char * dst_path ) {
diff --git a/src/builtin/module_builtin_ast_adapters.cpp b/src/builtin/module_builtin_ast_adapters.cpp
index 42615a8a55..8239ebce5e 100644
--- a/src/builtin/module_builtin_ast_adapters.cpp
+++ b/src/builtin/module_builtin_ast_adapters.cpp
@@ -2541,6 +2541,17 @@ namespace das {
program->visitModule(*adapter, module);
}
+ void astVisitModuleWithSort ( smart_ptr_raw program, VisitorAdapter * adapter,
+ Module* module, bool sortStructures, Context * context, LineInfoArg * line_info ) {
+ if (!adapter)
+ context->throw_error_at(line_info, "adapter is required");
+ if (!program)
+ context->throw_error_at(line_info, "program is required");
+ if (!module)
+ context->throw_error_at(line_info, "module is required");
+ program->visitModule(*adapter, module, false, sortStructures);
+ }
+
void astVisitModulesInOrder ( smart_ptr_raw program, VisitorAdapter * adapter, Context * context, LineInfoArg * line_info ) {
if (!adapter)
context->throw_error_at(line_info, "adapter is required");
@@ -2611,6 +2622,9 @@ namespace das {
addExtern(*this, lib, "visit_modules",
SideEffects::accessExternal, "astVisitModulesInOrder")
->args({"program","adapter","context","line"});
+ addExtern(*this, lib, "visit_module",
+ SideEffects::accessExternal, "astVisitModuleWithSort")
+ ->args({"program","adapter","module","sortStructures","context","lineInfo"});
addExtern(*this, lib, "visit_module",
SideEffects::accessExternal, "astVisitModule")
->args({"program","adapter","module","context","line"});
diff --git a/src/builtin/module_builtin_rtti.cpp b/src/builtin/module_builtin_rtti.cpp
index ddf1a04118..53c4f39355 100644
--- a/src/builtin/module_builtin_rtti.cpp
+++ b/src/builtin/module_builtin_rtti.cpp
@@ -974,6 +974,7 @@ namespace das {
addField("no_lint");
addField("no_init_check");
addField("export_all");
+ addField("export_public_functions");
addField("serialize_main_module");
addField("keep_alive");
addField("very_safe_context");
diff --git a/src/builtin/module_jit.cpp b/src/builtin/module_jit.cpp
index 1d2af19fc8..aecbd6a49d 100644
--- a/src/builtin/module_jit.cpp
+++ b/src/builtin/module_jit.cpp
@@ -376,8 +376,31 @@ namespace das {
#endif
return run_link_cmd(cmd.c_str(), libraryName, "Library", context);
}
+
+ bool create_static_library ( const char * objFilePath, const char * libraryName, const char * customTool, Context * context ) {
+ if ( !check_file_present(objFilePath) ) {
+ LOG(LogLevel::error) << "File '" << objFilePath << "' , containing compiled definitions, does not exist\n";
+ return false;
+ }
+ remove(libraryName);
+ std::string cmd;
+ #if defined(_WIN32) || defined(_WIN64)
+ #if defined(_MSC_VER)
+ const auto tool = find_linker(customTool, "llvm-lib.exe", "lib");
+ cmd = fmt::format(FMT_STRING("\"\"{}\" /nologo /OUT:\"{}\" \"{}\" 2>&1\""), tool.c_str(), libraryName, objFilePath);
+ #else
+ const auto tool = find_linker(customTool, "llvm-ar.exe", "ar");
+ cmd = fmt::format(FMT_STRING("\"\"{}\" rcs \"{}\" \"{}\" 2>&1\""), tool.c_str(), libraryName, objFilePath);
+ #endif
+ #else
+ const auto tool = find_linker(customTool, "llvm-ar", "ar");
+ cmd = fmt::format(FMT_STRING("\"{}\" rcs \"{}\" \"{}\" 2>&1"), tool.c_str(), libraryName, objFilePath);
+ #endif
+ return run_link_cmd(cmd.c_str(), libraryName, "Archive", context);
+ }
#else
bool create_shared_library ( const char * objFilePath, const char * libraryName, [[maybe_unused]] const char * dasLib, const char * customLinker, const char * extraLinkerArgs, bool isShared, bool linkWholeLib, bool debugInfo, Context *context ) { return true; }
+ bool create_static_library ( const char * objFilePath, const char * libraryName, const char * customTool, Context * context ) { return true; }
#endif
// ===== --jit-split-modules parallel optimize+emit =====
@@ -748,6 +771,9 @@ namespace das {
addExternInline(*this, lib, "create_shared_library",
SideEffects::worstDefault, "create_shared_library")
->args({"objFilePath","libraryName","dasLib","customLinker","extraLinkerArgs","isShared","linkWholeLib","debugInfo","context"});
+ addExternInline(*this, lib, "create_static_library",
+ SideEffects::worstDefault, "create_static_library")
+ ->args({"objFilePath","libraryName","customTool","context"});
addExternInline(*this, lib, "jit_par_emit_begin",
SideEffects::worstDefault, "jit_par_emit_begin");
addExternInline(*this, lib, "jit_par_emit_add",
diff --git a/tests-cpp/big/nano_ctx/CMakeLists.txt b/tests-cpp/big/nano_ctx/CMakeLists.txt
index 9cef9fba7c..6f8727cc54 100644
--- a/tests-cpp/big/nano_ctx/CMakeLists.txt
+++ b/tests-cpp/big/nano_ctx/CMakeLists.txt
@@ -44,6 +44,7 @@ foreach(_pair "01_pure/pure_math.das" "02_heap/heap_demo.das"
"${PROJECT_SOURCE_DIR}/utils/aot/main.das"
"${PROJECT_SOURCE_DIR}/daslib/aot_standalone.das"
"${PROJECT_SOURCE_DIR}/daslib/aot_cpp.das"
+ "${PROJECT_SOURCE_DIR}/daslib/c_api_header.das"
WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}"
COMMENT "Standalone AOT (nano): ${_das_name}"
VERBATIM
diff --git a/tests-cpp/big/nano_ctx/test_nano_ctx.cpp b/tests-cpp/big/nano_ctx/test_nano_ctx.cpp
index 5593b33889..6b80b6238e 100644
--- a/tests-cpp/big/nano_ctx/test_nano_ctx.cpp
+++ b/tests-cpp/big/nano_ctx/test_nano_ctx.cpp
@@ -78,12 +78,12 @@ int main () {
das_nano_set_print(&capture_print);
{ // tier A - POD compute, no das heap
- pure_math::Standalone ctx;
- pure_math::Vec3 a; a.x = 1.0f; a.y = 2.0f; a.z = 3.0f;
- pure_math::Vec3 b; b.x = 4.0f; b.y = 5.0f; b.z = 6.0f;
+ ctx_pure_math::Standalone ctx;
+ ctx_pure_math::Vec3 a; a.x = 1.0f; a.y = 2.0f; a.z = 3.0f;
+ ctx_pure_math::Vec3 b; b.x = 4.0f; b.y = 5.0f; b.z = 6.0f;
expect_float("dot", ctx.dot(a, b), 32.0f);
expect_float("scale.y", ctx.scale(a, 3.0f).y, 6.0f);
- expect_float("component(z)", ctx.component(a, pure_math::Axis::z), 3.0f);
+ expect_float("component(z)", ctx.component(a, ctx_pure_math::Axis::z), 3.0f);
expect_float("weighted_sum", ctx.weighted_sum(a), 2.0f);
expect_int("collatz_steps(27)", ctx.collatz_steps(27), 111);
// `options stack = 4096` is honored exactly, plus the headroom the
@@ -103,7 +103,7 @@ int main () {
}
{ // tier B - the das heap
- heap_demo::Standalone ctx;
+ ctx_heap_demo::Standalone ctx;
expect_int("sum_range(10)", ctx.sum_range(10), 285);
expect_int("histogram_peak(20)", ctx.histogram_peak(20), 3);
expect_int("alloc_and_free(8)", int(ctx.alloc_and_free(8)), 4);
@@ -112,7 +112,7 @@ int main () {
}
{ // tier C - lambdas, function pointers, generators
- closures::Standalone ctx;
+ ctx_closures::Standalone ctx;
expect_int("apply_twice(10)", ctx.apply_twice(10), 16);
expect_int("call_through_pointer(21)", ctx.call_through_pointer(21), 42);
expect_int("sum_squares(5)", ctx.sum_squares(5), 30);
@@ -122,7 +122,7 @@ int main () {
}
{ // output - `print` reaches the embedder's sink and nowhere else
- blinker::Standalone ctx;
+ ctx_blinker::Standalone ctx;
expect_int("lamp_pattern(3)", ctx.lamp_pattern(3), 8);
expect_int("lamp_pattern(4)", ctx.lamp_pattern(4), 4);
g_captured.clear();
@@ -175,7 +175,7 @@ int main () {
// that the numbers survived into the linked program at all: nothing in this
// binary can read thermistor.csv, so a wrong table cannot be recovered at
// run time - it can only be wrong.
- thermometer::Standalone ctx;
+ ctx_thermometer::Standalone ctx;
expect_int("baked table size", ctx.curve_size(), 64);
expect_int("baked ADC low", ctx.adc_range_lo(), 267);
expect_int("baked ADC high", ctx.adc_range_hi(), 3740);
diff --git a/tests-cpp/big/standalone_ctx/CMakeLists.txt b/tests-cpp/big/standalone_ctx/CMakeLists.txt
index 4879c2f353..03ee09c248 100644
--- a/tests-cpp/big/standalone_ctx/CMakeLists.txt
+++ b/tests-cpp/big/standalone_ctx/CMakeLists.txt
@@ -7,16 +7,22 @@ file(MAKE_DIRECTORY "${STANDALONE_CTX_GEN}")
add_custom_command(
OUTPUT "${STANDALONE_CTX_GEN}/standalone_init_fixture.das.cpp"
"${STANDALONE_CTX_GEN}/standalone_init_fixture.das.h"
+ "${CMAKE_CURRENT_SOURCE_DIR}/_standalone_init_fixture_c.das"
COMMAND $
"${PROJECT_SOURCE_DIR}/utils/aot/main.das"
-- -ctx "${CMAKE_CURRENT_SOURCE_DIR}/standalone_init_fixture.das"
"${STANDALONE_CTX_GEN}/"
+ # the bindings live beside the host that requires them; the library they name is the one
+ # the shared target below builds out of the same generated .cpp
+ -bindings "${CMAKE_CURRENT_SOURCE_DIR}/_standalone_init_fixture_c.das"
+ -bindings_library "${CMAKE_CURRENT_BINARY_DIR}/standalone_init_fixture"
DEPENDS daslang
"${CMAKE_CURRENT_SOURCE_DIR}/standalone_init_fixture.das"
"${CMAKE_CURRENT_SOURCE_DIR}/standalone_init_dep.das"
"${PROJECT_SOURCE_DIR}/utils/aot/main.das"
"${PROJECT_SOURCE_DIR}/daslib/aot_standalone.das"
"${PROJECT_SOURCE_DIR}/daslib/aot_cpp.das"
+ "${PROJECT_SOURCE_DIR}/daslib/c_api_header.das"
WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}"
COMMENT "Standalone AOT: standalone_init_fixture.das"
VERBATIM
@@ -36,6 +42,102 @@ add_test(NAME standalone_ctx COMMAND test_standalone_ctx
set_tests_properties(standalone_ctx PROPERTIES LABELS "big")
add_dependencies(test-big test_standalone_ctx)
+# The same fixture driven through its C API, with the C half of the generated header compiled by
+# a C compiler: a header that is not valid C, a layout that disagrees with das, or a thunk that
+# loses a value fails here. The C++ twin above shares the generated source, so both APIs are
+# proven against one emission.
+add_executable(test_standalone_capi
+ test_standalone_capi.c
+ "${STANDALONE_CTX_GEN}/standalone_init_fixture.das.cpp")
+target_link_libraries(test_standalone_capi PRIVATE
+ libDaScript ${SRC_LIBRARIES} ${DAS_MODULES_LIBS})
+target_include_directories(test_standalone_capi PRIVATE "${STANDALONE_CTX_GEN}" ${NEED_MODULES_PATH})
+set_target_properties(test_standalone_capi PROPERTIES
+ FOLDER "tests-cpp/big"
+ C_STANDARD 11
+ C_STANDARD_REQUIRED ON
+ LINKER_LANGUAGE CXX)
+SETUP_CPP11(test_standalone_capi)
+
+add_test(NAME standalone_capi COMMAND test_standalone_capi
+ WORKING_DIRECTORY ${PROJECT_SOURCE_DIR})
+set_tests_properties(standalone_capi PROPERTIES LABELS "small")
+add_dependencies(test-small test_standalone_capi)
+
+# The same emission, loaded instead of linked: a shared library built from the generated .cpp
+# exports the C entry points (default visibility; on Windows the header's _API macro is
+# what says so), and a daslang host reaches them through the bindings the same run wrote. Needs
+# the shared runtime, since the host process already carries one, and dasbind to load it -
+# DAS_BIND_EXTERNAL is 0 on 32-bit Windows (das_config.h), where the host cannot bind at all.
+if(TARGET libDaScriptDyn_runtime AND NOT (WIN32 AND CMAKE_SIZEOF_VOID_P EQUAL 4))
+ add_library(standalone_init_fixture_shared SHARED
+ "${STANDALONE_CTX_GEN}/standalone_init_fixture.das.cpp")
+ target_link_libraries(standalone_init_fixture_shared PRIVATE
+ libDaScriptDyn_runtime ${SRC_LIBRARIES} ${DAS_MODULES_LIBS})
+ target_include_directories(standalone_init_fixture_shared PRIVATE
+ "${STANDALONE_CTX_GEN}" ${NEED_MODULES_PATH})
+ target_compile_definitions(standalone_init_fixture_shared PRIVATE
+ STANDALONE_INIT_FIXTURE_SHARED STANDALONE_INIT_FIXTURE_BUILD)
+ # the shared runtime hides its typeinfo (SETUP_DAS_SHARED_LIBRARY, CXX_VISIBILITY_PRESET
+ # hidden), so a library carrying `class Standalone : public Context` cannot resolve
+ # typeinfo for das::Context at load. No RTTI, no reference - the same rule a shared module obeys.
+ target_compile_options(standalone_init_fixture_shared PRIVATE
+ $<$:-fno-rtti>
+ $<$:/GR->)
+ set_target_properties(standalone_init_fixture_shared PROPERTIES
+ PREFIX ""
+ OUTPUT_NAME standalone_init_fixture
+ LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
+ RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
+ BUILD_RPATH "${PROJECT_SOURCE_DIR}/lib"
+ FOLDER "tests-cpp/big")
+ SETUP_CPP11(standalone_init_fixture_shared)
+
+ add_test(NAME standalone_capi_dasbind
+ COMMAND $
+ "${CMAKE_CURRENT_SOURCE_DIR}/test_standalone_bindings_host.das"
+ WORKING_DIRECTORY ${PROJECT_SOURCE_DIR})
+ set_tests_properties(standalone_capi_dasbind PROPERTIES LABELS "small")
+ add_dependencies(test-small standalone_init_fixture_shared daslang)
+endif()
+
+# The generated header asserts every structure's size and each of its field offsets. Nothing
+# proved those asserts FIRE, which is the whole reason they are emitted, so this compiles a host
+# that packs its structures and requires the compiler to refuse it. Skipped on MSVC: the C flags
+# and the _Static_assert spelling both differ there.
+if(NOT MSVC)
+ add_custom_command(
+ OUTPUT "${STANDALONE_CTX_GEN}/standalone_layout_fixture.das.h"
+ "${STANDALONE_CTX_GEN}/standalone_layout_fixture.das.cpp"
+ COMMAND $
+ "${PROJECT_SOURCE_DIR}/utils/aot/main.das"
+ -- -ctx "${CMAKE_CURRENT_SOURCE_DIR}/standalone_layout_fixture.das"
+ "${STANDALONE_CTX_GEN}/"
+ DEPENDS daslang
+ "${CMAKE_CURRENT_SOURCE_DIR}/standalone_layout_fixture.das"
+ "${PROJECT_SOURCE_DIR}/utils/aot/main.das"
+ "${PROJECT_SOURCE_DIR}/daslib/aot_standalone.das"
+ "${PROJECT_SOURCE_DIR}/daslib/aot_cpp.das"
+ "${PROJECT_SOURCE_DIR}/daslib/c_api_header.das"
+ WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}"
+ COMMENT "Standalone AOT: standalone_layout_fixture.das"
+ VERBATIM
+ )
+ # ALL, not just a test-small dependency: CI runs `ctest -L small` without building that target
+ add_custom_target(standalone_layout_fixture_header ALL
+ DEPENDS "${STANDALONE_CTX_GEN}/standalone_layout_fixture.das.h")
+ set_target_properties(standalone_layout_fixture_header PROPERTIES FOLDER "tests-cpp/big")
+
+ add_test(NAME standalone_layout_assert COMMAND ${CMAKE_COMMAND}
+ -DCC=${CMAKE_C_COMPILER}
+ -DSRC=${CMAKE_CURRENT_SOURCE_DIR}/test_standalone_layout_packed.c
+ -DINC=${STANDALONE_CTX_GEN}
+ -DOUT=${CMAKE_CURRENT_BINARY_DIR}/packed_host.o
+ -P ${CMAKE_CURRENT_SOURCE_DIR}/expect_layout_assert.cmake)
+ set_tests_properties(standalone_layout_assert PROPERTIES LABELS "small")
+ add_dependencies(test-small standalone_layout_fixture_header)
+endif()
+
# Two contexts with different C++ module sets in one binary - the dasHV + fio example and a
# fio-only fixture - share one module registry lifetime. Small-labelled: a second owner of the
# shutdown, or an unbalanced Initialize, fails at exit and nothing else would notice.
@@ -54,6 +156,7 @@ if(NOT DAS_HV_DISABLED)
"${PROJECT_SOURCE_DIR}/utils/aot/main.das"
"${PROJECT_SOURCE_DIR}/daslib/aot_standalone.das"
"${PROJECT_SOURCE_DIR}/daslib/aot_cpp.das"
+ "${PROJECT_SOURCE_DIR}/daslib/c_api_header.das"
WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}"
COMMENT "Standalone AOT: ${_das_name}"
VERBATIM
diff --git a/tests-cpp/big/standalone_ctx/expect_layout_assert.cmake b/tests-cpp/big/standalone_ctx/expect_layout_assert.cmake
new file mode 100644
index 0000000000..222d16a98f
--- /dev/null
+++ b/tests-cpp/big/standalone_ctx/expect_layout_assert.cmake
@@ -0,0 +1,29 @@
+# Proves the generated header's layout guard fires. The packed host must fail to compile, and it
+# must fail ON a layout assert - any other error would let a broken guard pass this test.
+#
+# cmake -DCC= -DSRC= -DINC= -DOUT=