diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md deleted file mode 100644 index 55eaafb4e..000000000 --- a/.github/pull_request_template.md +++ /dev/null @@ -1,4 +0,0 @@ -These changes are made under both the "Apache 2.0" and the "GNU Lesser General -Public License 2.1 or later" license terms (dual license). - -SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later diff --git a/.github/pull_request_template.md.license b/.github/pull_request_template.md.license deleted file mode 100644 index f9c391ef5..000000000 --- a/.github/pull_request_template.md.license +++ /dev/null @@ -1,2 +0,0 @@ -SPDX-License-Identifier: CC0-1.0 -SPDX-FileCopyrightText: Davide Bettio diff --git a/.github/workflows/_fission_build_artifacts.yml b/.github/workflows/_fission_build_artifacts.yml new file mode 100644 index 000000000..29b823651 --- /dev/null +++ b/.github/workflows/_fission_build_artifacts.yml @@ -0,0 +1,59 @@ +name: Build artifacts + +on: + push: + tags: + - "*" + +permissions: + contents: write + +jobs: + wasm_build_web: + runs-on: ubuntu-24.04 + defaults: + run: + shell: bash + container: emscripten/emsdk + steps: + - name: ⬇️ Checkout repo + uses: actions/checkout@v4 + + - name: ⚙️ Install deps + run: sudo apt update -y && sudo apt install -y cmake gperf + + - name: 🏗️ Build WASM for web + working-directory: ./src/platforms/emscripten/ + run: | + set -euo pipefail + mkdir build + cd build + emcmake cmake .. -DAVM_EMSCRIPTEN_ENV=web + emmake make -j $(nproc) + + - name: Vars setup + id: setup + run: | + echo "artifacts_dir=src/platforms/emscripten/build/src" >> $GITHUB_OUTPUT + + - name: "Rename artifacts" + working-directory: ${{ steps.setup.outputs.artifacts_dir }} + run: | + gzip -k "AtomVM.mjs" + sha256sum "AtomVM.mjs" > "AtomVM.mjs.sha256" + gzip -k "AtomVM.wasm" + sha256sum "AtomVM.wasm" > "AtomVM.wasm.sha256" + + - name: "Release (web)" + uses: softprops/action-gh-release@v2 + with: + draft: ${{ startsWith( github.ref_name, 'test-') }} + prerelease: ${{ contains( github.ref_name, '-dev') || contains( github.ref_name, '-rc') }} + fail_on_unmatched_files: true + files: | + ${{ steps.setup.outputs.artifacts_dir }}/AtomVM.mjs + ${{ steps.setup.outputs.artifacts_dir }}/AtomVM.mjs.gz + ${{ steps.setup.outputs.artifacts_dir }}/AtomVM.mjs.sha256 + ${{ steps.setup.outputs.artifacts_dir }}/AtomVM.wasm + ${{ steps.setup.outputs.artifacts_dir }}/AtomVM.wasm.gz + ${{ steps.setup.outputs.artifacts_dir }}/AtomVM.wasm.sha256 diff --git a/.github/workflows/_fission_build_test.yml b/.github/workflows/_fission_build_test.yml new file mode 100644 index 000000000..bb84465a0 --- /dev/null +++ b/.github/workflows/_fission_build_test.yml @@ -0,0 +1,60 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Build & test + +on: [push, pull_request] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref != 'refs/heads/main' && github.ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + build: + name: Build & test + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v4 + - uses: webfactory/ssh-agent@v0.6.0 + with: + ssh-private-key: | + ${{ secrets.ACCESS_TO_ELIXIR_WASM }} + - name: Set up Elixir + uses: erlef/setup-beam@61e01a43a562a89bfc54c7f9a378ff67b03e4a21 # v1.16.0 + with: + elixir-version: "1.17.3" + otp-version: "26.0.2" + - name: Install deps + run: | + sudo apt install -y gperf libmbedtls-dev zlib1g-dev + - name: Build + run: | + echo "" > libs/CMakeLists.txt # disable stdlibs compilation, which hangs for unknown reasons + export PATH=$PATH:/home/runner/.mix/elixir/1-17/ # for rebar3 + mkdir build + cd build + cmake -DAVM_BUILD_RUNTIME_ONLY=ON -DSANITIZER=OFF -DDEBUG_GC=ON -DDEBUG_ASSERTIONS=ON .. + make -j $(nproc) + # - name: Test + # run: | + # cd build + # tests/test-erlang + # tests/test-enif + # tests/test-mailbox + # tests/test-structs + - name: Build Popcorn + run: | + git clone git@github.com:software-mansion/popcorn.git -b mf/downstream-atomvm + cd popcorn + echo "import Config; config :popcorn, target: :unix, runtime: {:path, \"../build/src\"}" > config/config.secret.exs + mix deps.get + export PATH=$PATH:/home/runner/.mix/elixir/1-17/ # for rebar3 + MIX_ENV=test mix popcorn.build_runtime --target unix + - name: Test Popcorn + run: cd popcorn && mix test diff --git a/.github/workflows/build-and-test-macos.yaml b/.github/workflows/build-and-test-macos.yaml index ccac90a4d..fa665af35 100644 --- a/.github/workflows/build-and-test-macos.yaml +++ b/.github/workflows/build-and-test-macos.yaml @@ -4,9 +4,12 @@ # SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later # +on: + workflow_dispatch: + name: Build and Test on macOS -on: +disabled: push: paths-ignore: - 'src/platforms/emscripten/**' diff --git a/.github/workflows/build-and-test-on-freebsd.yaml b/.github/workflows/build-and-test-on-freebsd.yaml index e5fabd623..8f52d6278 100644 --- a/.github/workflows/build-and-test-on-freebsd.yaml +++ b/.github/workflows/build-and-test-on-freebsd.yaml @@ -4,9 +4,12 @@ # SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later # +on: + workflow_dispatch: + name: build-and-test-on-freebsd -on: +disabled: push: paths-ignore: - 'src/platforms/emscripten/**' diff --git a/.github/workflows/build-and-test-other.yaml b/.github/workflows/build-and-test-other.yaml index 35747a8e3..ccae60084 100644 --- a/.github/workflows/build-and-test-other.yaml +++ b/.github/workflows/build-and-test-other.yaml @@ -4,9 +4,12 @@ # SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later # +on: + workflow_dispatch: + name: Build and Test on Other Architectures -on: +disabled: push: paths-ignore: - 'src/platforms/emscripten/**' diff --git a/.github/workflows/build-and-test.yaml b/.github/workflows/build-and-test.yaml index fcc45fd03..174e5ea5d 100644 --- a/.github/workflows/build-and-test.yaml +++ b/.github/workflows/build-and-test.yaml @@ -4,9 +4,12 @@ # SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later # +on: + workflow_dispatch: + name: Build and Test -on: +disabled: push: paths-ignore: - 'src/platforms/emscripten/**' diff --git a/.github/workflows/build-docs.yaml b/.github/workflows/build-docs.yaml index 2e4a098ad..6ac8ecc2f 100644 --- a/.github/workflows/build-docs.yaml +++ b/.github/workflows/build-docs.yaml @@ -3,13 +3,16 @@ # # SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later # + +on: + workflow_dispatch: # This is a workflow for atomvm/AtomVM to Publish API documentation and other content from the `doc` directory to # doc.atomvm.org hosted on GitHub Pages name: Build Docs # Controls when the workflow will run -on: +disabled: # Triggers the workflow on push request and tag events on main branch pull_request: tags: diff --git a/.github/workflows/build-libraries.yaml b/.github/workflows/build-libraries.yaml index 8ea723aa3..431a97262 100644 --- a/.github/workflows/build-libraries.yaml +++ b/.github/workflows/build-libraries.yaml @@ -4,9 +4,12 @@ # SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later # +on: + workflow_dispatch: + name: Build Libraries -on: +disabled: push: tags: - '**' diff --git a/.github/workflows/build-linux-artifacts.yaml b/.github/workflows/build-linux-artifacts.yaml index 55a8c76cb..e1223db26 100644 --- a/.github/workflows/build-linux-artifacts.yaml +++ b/.github/workflows/build-linux-artifacts.yaml @@ -4,9 +4,12 @@ # SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later # +on: + workflow_dispatch: + name: Build Linux Artifacts -on: +disabled: push: tags: - '**' diff --git a/.github/workflows/check-formatting.yaml b/.github/workflows/check-formatting.yaml index e87c83792..bcf312a4d 100644 --- a/.github/workflows/check-formatting.yaml +++ b/.github/workflows/check-formatting.yaml @@ -4,9 +4,12 @@ # SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later # +on: + workflow_dispatch: + name: "Check formatting" -on: +disabled: push: paths: - '.github/workflows/**' diff --git a/.github/workflows/codeql-analysis.yaml b/.github/workflows/codeql-analysis.yaml index aeae5644b..e090551cb 100644 --- a/.github/workflows/codeql-analysis.yaml +++ b/.github/workflows/codeql-analysis.yaml @@ -4,9 +4,12 @@ # SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later # +on: + workflow_dispatch: + name: "CodeQL" -on: +disabled: push: paths-ignore: - 'src/platforms/emscripten/**' diff --git a/.github/workflows/esp32-build.yaml b/.github/workflows/esp32-build.yaml index 5ca79dfa6..6b6ff218c 100644 --- a/.github/workflows/esp32-build.yaml +++ b/.github/workflows/esp32-build.yaml @@ -4,9 +4,12 @@ # SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later # +on: + workflow_dispatch: + name: ESP32 Builds -on: +disabled: push: paths: - '.github/workflows/esp32-build.yaml' diff --git a/.github/workflows/esp32-mkimage.yaml b/.github/workflows/esp32-mkimage.yaml index 52eeb55ac..406539e07 100644 --- a/.github/workflows/esp32-mkimage.yaml +++ b/.github/workflows/esp32-mkimage.yaml @@ -4,9 +4,12 @@ # SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later # +on: + workflow_dispatch: + name: esp32-mkimage -on: +disabled: push: paths: - '.github/workflows/esp32-mkimage.yaml' diff --git a/.github/workflows/esp32-simtest.yaml b/.github/workflows/esp32-simtest.yaml index c5c30d603..c9cdb0f21 100644 --- a/.github/workflows/esp32-simtest.yaml +++ b/.github/workflows/esp32-simtest.yaml @@ -5,9 +5,12 @@ # SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later # +on: + workflow_dispatch: + name: ESP32 Sim test -on: +disabled: push: paths: - ".github/workflows/esp32-simtest.yaml" diff --git a/.github/workflows/pico-build.yaml b/.github/workflows/pico-build.yaml index 9cf01d045..87c36fd7f 100644 --- a/.github/workflows/pico-build.yaml +++ b/.github/workflows/pico-build.yaml @@ -4,9 +4,12 @@ # SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later # +on: + workflow_dispatch: + name: Pico Build -on: +disabled: push: paths: - '.github/workflows/pico-build.yaml' diff --git a/.github/workflows/publish-docs.yaml b/.github/workflows/publish-docs.yaml index 6438ec9af..59ebd9ce2 100644 --- a/.github/workflows/publish-docs.yaml +++ b/.github/workflows/publish-docs.yaml @@ -3,13 +3,16 @@ # # SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later # + +on: + workflow_dispatch: # This is a workflow for atomvm/AtomVM to Publish API documentation and other content from the `doc` directory to # doc.atomvm.org hosted on GitHub Pages name: Publish Docs # Controls when the workflow will run -on: +disabled: # Triggers the workflow on pull request, tag events and pushes on main push: tags: diff --git a/.github/workflows/reuse-lint.yaml b/.github/workflows/reuse-lint.yaml index cf5e72a31..5176cf5bb 100644 --- a/.github/workflows/reuse-lint.yaml +++ b/.github/workflows/reuse-lint.yaml @@ -4,7 +4,7 @@ name: REUSE Compliance Check -on: [push, pull_request] +disabled: [push, pull_request] concurrency: group: ${{ github.workflow }}-${{ github.ref != 'refs/heads/main' && github.ref || github.run_id }} diff --git a/.github/workflows/run-tests-with-beam.yaml b/.github/workflows/run-tests-with-beam.yaml index e802db570..b4bbf721a 100644 --- a/.github/workflows/run-tests-with-beam.yaml +++ b/.github/workflows/run-tests-with-beam.yaml @@ -4,9 +4,12 @@ # SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later # +on: + workflow_dispatch: + name: Run tests with BEAM -on: +disabled: push: paths-ignore: - 'src/platforms/emscripten/**' diff --git a/.github/workflows/stm32-build.yaml b/.github/workflows/stm32-build.yaml index d8c745a1f..57a46debf 100644 --- a/.github/workflows/stm32-build.yaml +++ b/.github/workflows/stm32-build.yaml @@ -4,9 +4,12 @@ # SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later # +on: + workflow_dispatch: + name: STM32 Build -on: +disabled: push: paths: - '.github/workflows/stm32-build.yaml' diff --git a/.github/workflows/wasm-build.yaml b/.github/workflows/wasm-build.yaml index 5f1b17d9c..db69de913 100644 --- a/.github/workflows/wasm-build.yaml +++ b/.github/workflows/wasm-build.yaml @@ -4,9 +4,12 @@ # SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later # +on: + workflow_dispatch: + name: Wasm Build -on: +disabled: push: paths: - '.github/workflows/wasm-build.yaml' diff --git a/CMakeLists.txt b/CMakeLists.txt index 6ba351373..b15df85e7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,6 +28,8 @@ find_package(Erlang) find_package(Elixir) find_package(Gleam) +set(SANITIZER "address" CACHE STRING "Enable Address Sanitizer") # Include address, memory, or undefined sanitizer + option(AVM_DISABLE_FP "Disable floating point support." OFF) option(AVM_DISABLE_SMP "Disable SMP." OFF) option(AVM_DISABLE_TASK_DRIVER "Disable task driver support." OFF) diff --git a/src/libAtomVM/CMakeLists.txt b/src/libAtomVM/CMakeLists.txt index 2a5c58bd0..b55a0058c 100644 --- a/src/libAtomVM/CMakeLists.txt +++ b/src/libAtomVM/CMakeLists.txt @@ -106,6 +106,11 @@ set(SOURCE_FILES valueshashtable.c ) +file(GLOB_RECURSE POPCORN_HEADER_FILES "popcorn/*.{h,def}") +file(GLOB_RECURSE POPCORN_SOURCE_FILES "popcorn/*.c") +set(HEADER_FILES ${HEADER_FILES} ${POPCORN_HEADER_FILES}) +set(SOURCE_FILES ${SOURCE_FILES} ${POPCORN_SOURCE_FILES}) + if(NOT AVM_DISABLE_JIT) set(HEADER_FILES ${HEADER_FILES} jit.h @@ -143,6 +148,17 @@ if (ADVANCED_TRACING) target_compile_definitions(libAtomVM PUBLIC ENABLE_ADVANCED_TRACE) endif() +if(SANITIZER STREQUAL "address") + target_compile_options(libAtomVM PUBLIC -fsanitize=address -fno-omit-frame-pointer) + target_link_options(libAtomVM PUBLIC -fsanitize=address) +elseif(SANITIZER STREQUAL "memory") + target_compile_options(libAtomVM PUBLIC -fsanitize=memory -fno-omit-frame-pointer) + target_link_options(libAtomVM PUBLIC -fsanitize=memory) +elseif(SANITIZER STREQUAL "undefined") + target_compile_options(libAtomVM PUBLIC -fsanitize=undefined) + target_link_options(libAtomVM PUBLIC -fsanitize=undefined) +endif() + target_link_libraries(libAtomVM PUBLIC m) include(CheckCSourceCompiles) set(CMAKE_REQUIRED_FLAGS "${CMAKE_REQUIRED_FLAGS} -Werror=unknown-pragmas") @@ -282,6 +298,9 @@ gperf_generate(${CMAKE_CURRENT_SOURCE_DIR}/nifs.gperf nifs_hash.h) add_custom_target(generated DEPENDS bifs_hash.h) add_custom_target(generated-nifs-hash DEPENDS nifs_hash.h) +gperf_generate(${CMAKE_CURRENT_SOURCE_DIR}/popcorn/popcorn_nifs.gperf popcorn_nifs_hash.h) +add_custom_target(generated-popcorn-nifs-hash DEPENDS popcorn_nifs_hash.h) + include(../../version.cmake) if (ATOMVM_DEV) @@ -312,6 +331,8 @@ endif() add_dependencies(libAtomVM generated generated-nifs-hash) +add_dependencies(libAtomVM generated generated-popcorn-nifs-hash) + if (COVERAGE) include(CodeCoverage) append_coverage_compiler_flags_to_target(libAtomVM) diff --git a/src/libAtomVM/bifs.gperf b/src/libAtomVM/bifs.gperf index c8b6bf2ac..580935933 100644 --- a/src/libAtomVM/bifs.gperf +++ b/src/libAtomVM/bifs.gperf @@ -37,6 +37,7 @@ struct BifNameAndPtr %% erlang:self/0, {.bif.base.type = BIFFunctionType, .bif.bif0_ptr = bif_erlang_self_0} erlang:node/0, {.bif.base.type = BIFFunctionType, .bif.bif0_ptr = bif_erlang_node_0} +erlang:node/1, {.bif.base.type = BIFFunctionType, .bif.bif0_ptr = bif_erlang_node_0} erlang:length/1, {.gcbif.base.type = GCBIFFunctionType, .gcbif.gcbif1_ptr = bif_erlang_length_1} erlang:byte_size/1, {.gcbif.base.type = GCBIFFunctionType, .gcbif.gcbif1_ptr = bif_erlang_byte_size_1} erlang:bit_size/1, {.gcbif.base.type = GCBIFFunctionType, .gcbif.gcbif1_ptr = bif_erlang_bit_size_1} diff --git a/src/libAtomVM/context.c b/src/libAtomVM/context.c index f682e8c34..cf17c1aef 100644 --- a/src/libAtomVM/context.c +++ b/src/libAtomVM/context.c @@ -289,6 +289,7 @@ void context_destroy(Context *ctx) free(ctx->platform_data); ets_delete_owned_tables(&ctx->global->ets, ctx->process_id, ctx->global); + popcorn_ets_delete_owned_tables(&ctx->global->popcorn_ets, ctx->process_id, ctx->global); free(ctx); } diff --git a/src/libAtomVM/defaultatoms.c b/src/libAtomVM/defaultatoms.c index feeec3c9f..7b2eb9ca3 100644 --- a/src/libAtomVM/defaultatoms.c +++ b/src/libAtomVM/defaultatoms.c @@ -33,6 +33,7 @@ void defaultatoms_init(GlobalContext *glb) static const char *const atoms[] = { #include "defaultatoms.def" +#include "popcorn/popcorn_atoms.def" // dummy value NULL diff --git a/src/libAtomVM/defaultatoms.h b/src/libAtomVM/defaultatoms.h index cf033730e..62033ad95 100644 --- a/src/libAtomVM/defaultatoms.h +++ b/src/libAtomVM/defaultatoms.h @@ -35,6 +35,7 @@ extern "C" { enum { #include "defaultatoms.def" +#include "popcorn/popcorn_atoms.def" // The first index for platform specific atoms, should always be last in the list PLATFORM_ATOMS_BASE_INDEX @@ -51,6 +52,7 @@ _Static_assert(TRUE_ATOM_INDEX == 1, "true atom index must be 1"); enum { #include "defaultatoms.def" +#include "popcorn/popcorn_atoms.def" // dummy last item PLATFORM_ATOMS_BASE_DUMMY = TERM_FROM_ATOM_INDEX(PLATFORM_ATOMS_BASE_INDEX) diff --git a/src/libAtomVM/globalcontext.c b/src/libAtomVM/globalcontext.c index ad1a2d028..64c45c088 100644 --- a/src/libAtomVM/globalcontext.c +++ b/src/libAtomVM/globalcontext.c @@ -84,6 +84,7 @@ GlobalContext *globalcontext_new(void) synclist_init(&glb->select_events); ets_init(&glb->ets); + popcorn_ets_init(&glb->popcorn_ets); glb->last_process_id = 0; @@ -230,6 +231,7 @@ COLD_FUNC void globalcontext_destroy(GlobalContext *glb) synclist_destroy(&glb->select_events); ets_destroy(&glb->ets, glb); + popcorn_ets_destroy(&glb->popcorn_ets, glb); // Destroy refc binaries including resources // (this list should be empty if resources were properly refcounted) diff --git a/src/libAtomVM/globalcontext.h b/src/libAtomVM/globalcontext.h index 3501195c1..2a0aed3c5 100644 --- a/src/libAtomVM/globalcontext.h +++ b/src/libAtomVM/globalcontext.h @@ -30,6 +30,8 @@ #include +#include "popcorn/popcorn_ets.h" + #include "atom.h" #include "atom_table.h" #include "erl_nif.h" @@ -121,6 +123,7 @@ struct GlobalContext struct SyncList select_events; struct Ets ets; + struct PopcornEts popcorn_ets; int32_t last_process_id; diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index 2fe0b1294..316a83d2a 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -24,6 +24,8 @@ #include "nifs.h" +#include "popcorn/popcorn_nifs.h" + #include #include #include @@ -924,6 +926,11 @@ DEFINE_MATH_NIF(tanh) const struct Nif *nifs_get(const char *mfa) { + const struct Nif *nif = popcorn_nifs_get_nif(mfa); + if (nif) { + return nif; + } + const NifNameAndNifPtr *nameAndPtr = nif_in_word_set(mfa, strlen(mfa)); if (!nameAndPtr) { return platform_nifs_get_nif(mfa); @@ -5666,7 +5673,8 @@ static term nif_erlang_nif_error(Context *ctx, int argc, term argv[]) UNUSED(argc); UNUSED(argv); - RAISE_ERROR(UNDEF_ATOM); + fprintf(stderr, "Nif not found, aborting\n"); + AVM_ABORT(); } #ifndef AVM_NO_JIT diff --git a/src/libAtomVM/opcodesswitch.h b/src/libAtomVM/opcodesswitch.h index d8fc4106b..b2d4ac590 100644 --- a/src/libAtomVM/opcodesswitch.h +++ b/src/libAtomVM/opcodesswitch.h @@ -224,10 +224,14 @@ typedef dreg_t dreg_gc_safe_t; case COMPACT_NBITS_VALUE:{ \ int sz = (first_byte >> 5) + 2; \ if (UNLIKELY(sz > 8)) { \ - /* TODO: when first_byte >> 5 is 7, a different encoding is used */ \ - fprintf(stderr, "Unexpected nbits vaue @ %" PRIuPTR "\n", (uintptr_t) ((decode_pc) - 1)); \ - AVM_ABORT(); \ - break; \ + sz = *(decode_pc) + 9; \ + (decode_pc)++; \ + /* Integer larger than 60-bits but no bigger than 64-bits */ \ + /* will become boxed term taking 9 bytes (1 byte box + 64 bits)*/ \ + /* AtomVM can handle 64 bit int, but not larger ones until BigNum support is ready*/ \ + if (sz > 9 && (first_byte & 0xF) == COMPACT_LARGE_INTEGER) { \ + fprintf(stderr, "WARNING: Loading integer possibly longer than 64 bits"); \ + } \ } \ (decode_pc) += sz; \ break; \ diff --git a/src/libAtomVM/popcorn/popcorn_atoms.def b/src/libAtomVM/popcorn/popcorn_atoms.def new file mode 100644 index 000000000..2cda1855b --- /dev/null +++ b/src/libAtomVM/popcorn/popcorn_atoms.def @@ -0,0 +1,4 @@ +X(TRIM_ATOM, "\x4", "trim") +X(TRIM_ALL_ATOM, "\x8", "trim_all") +X(STDOUT_ATOM, "\x6", "stdout") +X(STDERR_ATOM, "\x6", "stderr") diff --git a/src/libAtomVM/popcorn/popcorn_ets.c b/src/libAtomVM/popcorn/popcorn_ets.c new file mode 100644 index 000000000..b12af5f7e --- /dev/null +++ b/src/libAtomVM/popcorn/popcorn_ets.c @@ -0,0 +1,829 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2024 Fred Dushin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later + */ + +#include "popcorn_ets.h" + +#include "context.h" +#include "defaultatoms.h" +#include "list.h" +#include "memory.h" +#include "popcorn_ets_hashtable.h" +#include "term.h" +#include "utils.h" +#include + +#define ETS_NO_INDEX SIZE_MAX +#define ETS_ANY_PROCESS -1 + +#ifndef AVM_NO_SMP +#define SMP_RDLOCK(table) smp_rwlock_rdlock(table->lock) +#define SMP_WRLOCK(table) smp_rwlock_wrlock(table->lock) +#define SMP_UNLOCK(table) smp_rwlock_unlock(table->lock) +#else +#define SMP_RDLOCK(table) +#define SMP_WRLOCK(table) +#define SMP_UNLOCK(table) +#endif + +#ifndef AVM_NO_SMP +#ifndef TYPEDEF_RWLOCK +#define TYPEDEF_RWLOCK +typedef struct RWLock RWLock; +#endif +#endif + +struct PopcornEtsTable +{ + struct ListHead head; + uint64_t ref_ticks; + term name; + bool is_named; + int32_t owner_process_id; + size_t key_index; + PopcornEtsTableType table_type; + // In the future, we might support rb-trees for sorted spopcorn_ets + // For this MVP, we only support unsorted spopcorn_ets + struct PopcornEtsHashTable *hashtable; + PopcornEtsAccessType access_type; + +#ifndef AVM_NO_SMP + RWLock *lock; +#endif +}; +typedef enum TableAccessType +{ + TableAccessNone, + TableAccessRead, + TableAccessWrite +} TableAccessType; + +static void popcorn_ets_delete_all_tables(struct PopcornEts *popcorn_ets, GlobalContext *global); + +static void popcorn_ets_add_table(struct PopcornEts *popcorn_ets, struct PopcornEtsTable *popcorn_ets_table) +{ + struct ListHead *popcorn_ets_tables_list = synclist_wrlock(&popcorn_ets->popcorn_ets_tables); + + list_append(popcorn_ets_tables_list, &popcorn_ets_table->head); + + synclist_unlock(&popcorn_ets->popcorn_ets_tables); +} + +static struct PopcornEtsTable *popcorn_ets_get_table(struct PopcornEts *popcorn_ets, int64_t process_id, term name_or_ref, TableAccessType access_type) +{ + struct ListHead *popcorn_ets_tables_list = synclist_rdlock(&popcorn_ets->popcorn_ets_tables); + struct ListHead *item; + struct PopcornEtsTable *ret = NULL; + + uint64_t ref = 0; + term name = term_invalid_term(); + bool is_atom = term_is_atom(name_or_ref); + if (is_atom) { + name = name_or_ref; + } else { + ref = term_to_ref_ticks(name_or_ref); + } + + LIST_FOR_EACH (item, popcorn_ets_tables_list) { + struct PopcornEtsTable *table = GET_LIST_ENTRY(item, struct PopcornEtsTable, head); + bool found = is_atom ? table->is_named && table->name == name : table->ref_ticks == ref; + if (found) { + bool is_owner = table->owner_process_id == process_id; + bool can_read = access_type == TableAccessRead && (table->access_type != PopcornEtsAccessPrivate || is_owner); + bool can_write = access_type == TableAccessWrite && (table->access_type == PopcornEtsAccessPublic || is_owner); + bool access_none = access_type == TableAccessNone; + if (can_read) { + SMP_RDLOCK(table); + ret = table; + } else if (can_write) { + SMP_WRLOCK(table); + ret = table; + } else if (access_none) { + ret = table; + } + break; + } + } + synclist_unlock(&popcorn_ets->popcorn_ets_tables); + return ret; +} + +void popcorn_ets_init(struct PopcornEts *popcorn_ets) +{ + synclist_init(&popcorn_ets->popcorn_ets_tables); +} + +void popcorn_ets_destroy(struct PopcornEts *popcorn_ets, GlobalContext *global) +{ + popcorn_ets_delete_all_tables(popcorn_ets, global); + synclist_destroy(&popcorn_ets->popcorn_ets_tables); +} + +PopcornEtsErrorCode popcorn_ets_create_table(term name, bool is_named, PopcornEtsTableType table_type, PopcornEtsAccessType access_type, size_t key_index, term *ret, Context *ctx) +{ + if (is_named) { + struct PopcornEtsTable *popcorn_ets_table = popcorn_ets_get_table(&ctx->global->popcorn_ets, ETS_ANY_PROCESS, name, TableAccessNone); + if (popcorn_ets_table != NULL) { + return PopcornEtsTableNameInUse; + } + } + + struct PopcornEtsTable *popcorn_ets_table = malloc(sizeof(struct PopcornEtsTable)); + if (IS_NULL_PTR(popcorn_ets_table)) { + return PopcornEtsAllocationFailure; + } + + list_init(&popcorn_ets_table->head); + + popcorn_ets_table->name = name; + popcorn_ets_table->is_named = is_named; + popcorn_ets_table->access_type = access_type; + + popcorn_ets_table->table_type = table_type; + struct PopcornEtsHashTable *hashtable = popcorn_ets_hashtable_new(); + if (IS_NULL_PTR(hashtable)) { + free(popcorn_ets_table); + return PopcornEtsAllocationFailure; + } + popcorn_ets_table->hashtable = hashtable; + + popcorn_ets_table->owner_process_id = ctx->process_id; + + uint64_t ref_ticks = globalcontext_get_ref_ticks(ctx->global); + popcorn_ets_table->ref_ticks = ref_ticks; + + popcorn_ets_table->key_index = key_index; + +#ifndef AVM_NO_SMP + popcorn_ets_table->lock = smp_rwlock_create(); +#endif + + if (is_named) { + *ret = name; + } else { + if (UNLIKELY(memory_ensure_free_opt(ctx, REF_SIZE, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + popcorn_ets_hashtable_destroy(hashtable, ctx->global); + free(popcorn_ets_table); + return PopcornEtsAllocationFailure; + } + *ret = term_from_ref_ticks(ref_ticks, &ctx->heap); + } + + popcorn_ets_add_table(&ctx->global->popcorn_ets, popcorn_ets_table); + + return PopcornEtsOk; +} + +static void popcorn_ets_table_destroy(struct PopcornEtsTable *table, GlobalContext *global) +{ + SMP_WRLOCK(table); + popcorn_ets_hashtable_destroy(table->hashtable, global); + SMP_UNLOCK(table); + +#ifndef AVM_NO_SMP + smp_rwlock_destroy(table->lock); +#endif + + free(table); +} + +typedef bool (*popcorn_ets_table_filter_pred)(struct PopcornEtsTable *table, void *data); + +static void popcorn_ets_delete_tables_internal(struct PopcornEts *popcorn_ets, popcorn_ets_table_filter_pred pred, void *data, GlobalContext *global) +{ + struct ListHead *popcorn_ets_tables_list = synclist_wrlock(&popcorn_ets->popcorn_ets_tables); + struct ListHead *item; + struct ListHead *tmp; + MUTABLE_LIST_FOR_EACH (item, tmp, popcorn_ets_tables_list) { + struct PopcornEtsTable *table = GET_LIST_ENTRY(item, struct PopcornEtsTable, head); + if (pred(table, data)) { + list_remove(&table->head); + popcorn_ets_table_destroy(table, global); + } + } + synclist_unlock(&popcorn_ets->popcorn_ets_tables); +} + +static bool equal_process_id_pred(struct PopcornEtsTable *table, void *data) +{ + int32_t *process_id = (int32_t *) data; + return table->owner_process_id == *process_id; +} + +void popcorn_ets_delete_owned_tables(struct PopcornEts *popcorn_ets, int32_t process_id, GlobalContext *global) +{ + popcorn_ets_delete_tables_internal(popcorn_ets, equal_process_id_pred, &process_id, global); +} + +static bool true_pred(struct PopcornEtsTable *table, void *data) +{ + UNUSED(table); + UNUSED(data); + + return true; +} + +static void popcorn_ets_delete_all_tables(struct PopcornEts *popcorn_ets, GlobalContext *global) +{ + popcorn_ets_delete_tables_internal(popcorn_ets, true_pred, NULL, global); +} + +static bool popcorn_ets_hashtable_new_heap(size_t size, Heap **new_heap) +{ + Heap *heap = malloc(sizeof(Heap)); + if (IS_NULL_PTR(heap)) { + return false; + } + + if (UNLIKELY(memory_init_heap(heap, size) != MEMORY_GC_OK)) { + free(heap); + return false; + } + + *new_heap = heap; + return true; +} + +static PopcornEtsErrorCode popcorn_ets_insert_internal(struct PopcornEtsTable *popcorn_ets_table, term tuple, bool *tuple_inserted, Context *ctx) +{ + size_t arity = (size_t) term_get_tuple_arity(tuple); + if (popcorn_ets_table->key_index >= arity) { + return PopcornEtsBadEntry; + } + + bool is_duplicate_bag = popcorn_ets_table->table_type == PopcornEtsTableDuplicateBag; + bool insert_new = tuple_inserted != NULL; + + term popcorn_ets_tuple; + term popcorn_ets_key; + Heap *popcorn_ets_heap; + + if (is_duplicate_bag) { + // With duplicate bag mode, we copy entire entries list to new heap fragment. + // We could create a new heap and merge it with the existing one but we'd need to expose nodes from hashtable. + // Alternatively, we could use owner's heap, as popcorn_ets table shouldn't be accessible after owner exited. + term tuple_key = term_get_tuple_element(tuple, (int) popcorn_ets_table->key_index); + term old_tuples = popcorn_ets_hashtable_lookup(popcorn_ets_table->hashtable, tuple_key, ctx->global); + size_t size = memory_estimate_usage(tuple) + memory_estimate_usage(old_tuples) + CONS_SIZE; + if (UNLIKELY(!popcorn_ets_hashtable_new_heap(size, &popcorn_ets_heap))) { + return PopcornEtsAllocationFailure; + } + term popcorn_ets_tuples = memory_copy_term_tree(popcorn_ets_heap, old_tuples); + popcorn_ets_tuple = memory_copy_term_tree(popcorn_ets_heap, tuple); + + popcorn_ets_key = term_get_tuple_element(popcorn_ets_tuple, (int) popcorn_ets_table->key_index); + popcorn_ets_tuple = term_list_prepend(popcorn_ets_tuple, popcorn_ets_tuples, popcorn_ets_heap); + } else { + size_t size = memory_estimate_usage(tuple); + if (!popcorn_ets_hashtable_new_heap(size, &popcorn_ets_heap)) { + return PopcornEtsAllocationFailure; + } + + popcorn_ets_tuple = memory_copy_term_tree(popcorn_ets_heap, tuple); + popcorn_ets_key = term_get_tuple_element(popcorn_ets_tuple, (int) popcorn_ets_table->key_index); + } + + PopcornEtsHashtableOptions opts = insert_new ? 0 : PopcornEtsHashtableAllowOverwrite; + PopcornEtsHashtableErrorCode res = popcorn_ets_hashtable_insert(popcorn_ets_table->hashtable, popcorn_ets_key, popcorn_ets_tuple, opts, popcorn_ets_heap, ctx->global); + if (insert_new && res == PopcornEtsHashtableOk) { + *tuple_inserted = true; + return PopcornEtsOk; + } else if (insert_new && res == PopcornEtsHashtableKeyAlreadyExists) { + *tuple_inserted = false; + memory_destroy_heap(popcorn_ets_heap, ctx->global); + return PopcornEtsOk; + } else if (UNLIKELY(res != PopcornEtsHashtableOk)) { + memory_destroy_heap(popcorn_ets_heap, ctx->global); + return PopcornEtsAllocationFailure; + } + return PopcornEtsOk; +} + +static PopcornEtsErrorCode popcorn_ets_insert_multiple_internal(struct PopcornEtsTable *popcorn_ets_table, term entries, bool *overwritten, Context *ctx) +{ + bool insert_new = overwritten != NULL; + term iter = entries; + while (!term_is_nil(iter)) { + term entry = term_get_list_head(iter); + bool bad_pos = !term_is_tuple(entry) || popcorn_ets_table->key_index >= (size_t) term_get_tuple_arity(entry); + if (bad_pos) { + return PopcornEtsBadEntry; + } + + if (insert_new) { + term key = term_get_tuple_element(entry, popcorn_ets_table->key_index); + term res = popcorn_ets_hashtable_lookup(popcorn_ets_table->hashtable, key, ctx->global); + bool exists = !term_is_nil(res); + if (exists) { + *overwritten = false; + return PopcornEtsOk; + } + } + + iter = term_get_list_tail(iter); + } + + while (term_is_nonempty_list(entries)) { + term entry = term_get_list_head(entries); + PopcornEtsErrorCode result = popcorn_ets_insert_internal(popcorn_ets_table, entry, overwritten, ctx); + if (UNLIKELY(result != PopcornEtsOk)) { + // Partially inserted list + // We would need to save previous values (i.e. memory) and reverting can fail (i.e. memory) + AVM_ABORT(); + } + + entries = term_get_list_tail(entries); + } + + return PopcornEtsOk; +} + +PopcornEtsErrorCode popcorn_ets_insert(term ref, term entry, bool *entry_inserted, Context *ctx) +{ + struct PopcornEtsTable *popcorn_ets_table = popcorn_ets_get_table(&ctx->global->popcorn_ets, ctx->process_id, ref, TableAccessWrite); + if (popcorn_ets_table == NULL) { + return PopcornEtsBadAccess; + } + + PopcornEtsErrorCode result = PopcornEtsBadEntry; + + if (term_is_tuple(entry)) { + result = popcorn_ets_insert_internal(popcorn_ets_table, entry, entry_inserted, ctx); + } else if (term_is_list(entry)) { + result = popcorn_ets_insert_multiple_internal(popcorn_ets_table, entry, entry_inserted, ctx); + } + + SMP_UNLOCK(popcorn_ets_table); + + return result; +} + +static PopcornEtsErrorCode popcorn_ets_lookup_internal(struct PopcornEtsTable *popcorn_ets_table, term key, size_t index, term *ret, Context *ctx) +{ + bool is_duplicate_bag = popcorn_ets_table->table_type == PopcornEtsTableDuplicateBag; + bool lookup_element = index != ETS_NO_INDEX; + + term popcorn_ets_entry = popcorn_ets_hashtable_lookup(popcorn_ets_table->hashtable, key, ctx->global); + + if (term_is_nil(popcorn_ets_entry)) { + *ret = term_nil(); + } else if (is_duplicate_bag) { + // for tuple list and it reversed version - we don't want to copy terms in the loop + size_t size = 2 * memory_estimate_usage(popcorn_ets_entry); + // we don't need to preserve tuples, they live on different heap + if (UNLIKELY(memory_ensure_free_opt(ctx, size, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + return PopcornEtsAllocationFailure; + } + term tuples = memory_copy_term_tree(&ctx->heap, popcorn_ets_entry); + // lookup returns in insertion order + // TODO: store it in correct order? + term reversed = term_nil(); + while (!term_is_nil(tuples)) { + term tuple = term_get_list_head(tuples); + if (lookup_element) { + if (index >= (size_t) term_get_tuple_arity(tuple)) { + return PopcornEtsBadPosition; + } + tuple = term_get_tuple_element(tuple, index); + } + reversed = term_list_prepend(tuple, reversed, &ctx->heap); + tuples = term_get_list_tail(tuples); + } + + *ret = reversed; + } else { + if (lookup_element) { + if (index >= (size_t) term_get_tuple_arity(popcorn_ets_entry)) { + return PopcornEtsBadPosition; + } + popcorn_ets_entry = term_get_tuple_element(popcorn_ets_entry, index); + } + size_t size = (size_t) memory_estimate_usage(popcorn_ets_entry) + CONS_SIZE; + if (UNLIKELY(memory_ensure_free_opt(ctx, size, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + return PopcornEtsAllocationFailure; + } + term tuple = memory_copy_term_tree(&ctx->heap, popcorn_ets_entry); + + *ret = term_list_prepend(tuple, term_nil(), &ctx->heap); + } + + return PopcornEtsOk; +} + +PopcornEtsErrorCode popcorn_ets_lookup(term ref, term key, term *ret, Context *ctx) +{ + struct PopcornEtsTable *popcorn_ets_table = popcorn_ets_get_table(&ctx->global->popcorn_ets, ctx->process_id, ref, TableAccessRead); + if (popcorn_ets_table == NULL) { + return PopcornEtsBadAccess; + } + + PopcornEtsErrorCode result = popcorn_ets_lookup_internal(popcorn_ets_table, key, ETS_NO_INDEX, ret, ctx); + SMP_UNLOCK(popcorn_ets_table); + + return result; +} + +PopcornEtsErrorCode popcorn_ets_lookup_element(term ref, term key, size_t index, term *ret, Context *ctx) +{ + struct PopcornEtsTable *popcorn_ets_table = popcorn_ets_get_table(&ctx->global->popcorn_ets, ctx->process_id, ref, TableAccessRead); + if (popcorn_ets_table == NULL) { + return PopcornEtsBadAccess; + } + + bool is_duplicate_bag = popcorn_ets_table->table_type == PopcornEtsTableDuplicateBag; + + term entry; + PopcornEtsErrorCode result = popcorn_ets_lookup_internal(popcorn_ets_table, key, index, &entry, ctx); + if (result != PopcornEtsOk) { + SMP_UNLOCK(popcorn_ets_table); + return result; + } + if (term_is_nil(entry)) { + SMP_UNLOCK(popcorn_ets_table); + return PopcornEtsEntryNotFound; + } + + if (is_duplicate_bag) { + *ret = entry; + } else { + *ret = term_get_list_head(entry); + } + SMP_UNLOCK(popcorn_ets_table); + + return PopcornEtsOk; +} + +PopcornEtsErrorCode popcorn_ets_drop_table(term ref, term *ret, Context *ctx) +{ + struct PopcornEtsTable *popcorn_ets_table = popcorn_ets_get_table(&ctx->global->popcorn_ets, ctx->process_id, ref, TableAccessWrite); + if (popcorn_ets_table == NULL) { + return PopcornEtsBadAccess; + } + + synclist_wrlock(&ctx->global->popcorn_ets.popcorn_ets_tables); + SMP_UNLOCK(popcorn_ets_table); + list_remove(&popcorn_ets_table->head); + popcorn_ets_table_destroy(popcorn_ets_table, ctx->global); + synclist_unlock(&ctx->global->popcorn_ets.popcorn_ets_tables); + + *ret = TRUE_ATOM; + return PopcornEtsOk; +} + +PopcornEtsErrorCode popcorn_ets_delete(term ref, term key, term *ret, Context *ctx) +{ + struct PopcornEtsTable *popcorn_ets_table = popcorn_ets_get_table(&ctx->global->popcorn_ets, ctx->process_id, ref, TableAccessWrite); + if (popcorn_ets_table == NULL) { + return PopcornEtsBadAccess; + } + + bool _found = popcorn_ets_hashtable_remove(popcorn_ets_table->hashtable, key, NULL, ctx->global); + UNUSED(_found); + SMP_UNLOCK(popcorn_ets_table); + + *ret = TRUE_ATOM; + return PopcornEtsOk; +} + +PopcornEtsErrorCode popcorn_ets_delete_object(term ref, term tuple, term *ret, Context *ctx) +{ + PopcornEtsErrorCode error_code = PopcornEtsOk; + + struct PopcornEtsTable *popcorn_ets_table = popcorn_ets_get_table(&ctx->global->popcorn_ets, ctx->process_id, ref, TableAccessWrite); + if (popcorn_ets_table == NULL) { + error_code = PopcornEtsBadAccess; + goto exit; + } + + bool is_duplicate_bag = popcorn_ets_table->table_type == PopcornEtsTableDuplicateBag; + + term index = popcorn_ets_table->key_index; + if (index >= (size_t) term_get_tuple_arity(tuple)) { + error_code = PopcornEtsBadPosition; + goto exit; + } + term key = term_get_tuple_element(tuple, index); + + if (is_duplicate_bag) { + term entries = popcorn_ets_hashtable_lookup(popcorn_ets_table->hashtable, key, ctx->global); + if (term_is_nil(entries)) { + goto exit; + } + + int proper; + size_t n = term_list_length(entries, &proper); + UNUSED(proper); + + term *kept = malloc(n * sizeof(term)); + size_t kept_n = 0; + while (!term_is_nil(entries)) { + term entry = term_get_list_head(entries); + + // full element comparison + TermCompareResult cmp = term_compare(entry, tuple, TermCompareExact, ctx->global); + + bool keep = cmp != TermEquals; + if (UNLIKELY(cmp == TermCompareMemoryAllocFail)) { + error_code = PopcornEtsAllocationFailure; + goto exit; + } + if (keep) { + kept[kept_n++] = entry; + } + + entries = term_get_list_tail(entries); + } + + bool all_removed = kept_n == 0; + if (all_removed) { + bool _found = popcorn_ets_hashtable_remove(popcorn_ets_table->hashtable, key, NULL, ctx->global); + UNUSED(_found); + free(kept); + } else { + size_t memory_needed = memory_estimate_usage(key); + for (size_t i = 0; i < kept_n; ++i) { + memory_needed += memory_estimate_usage(kept[i]) + CONS_SIZE; + } + + Heap *heap; + if (UNLIKELY(!popcorn_ets_hashtable_new_heap(memory_needed, &heap))) { + error_code = PopcornEtsAllocationFailure; + goto exit; + } + + term filtered = term_nil(); + for (size_t i = 0; i < kept_n; ++i) { + term copy = memory_copy_term_tree(heap, kept[i]); + filtered = term_list_prepend(copy, filtered, heap); + } + free(kept); + + term new_key = memory_copy_term_tree(heap, key); + PopcornEtsHashtableErrorCode res = popcorn_ets_hashtable_insert(popcorn_ets_table->hashtable, new_key, filtered, PopcornEtsHashtableAllowOverwrite, heap, ctx->global); + if (res != PopcornEtsHashtableOk) { + error_code = PopcornEtsAllocationFailure; + goto exit; + } + } + } else { + term entry = popcorn_ets_hashtable_lookup(popcorn_ets_table->hashtable, key, ctx->global); + if (term_is_nil(entry)) { + goto exit; + } + // full element comparison + TermCompareResult cmp = term_compare(entry, tuple, TermCompareExact, ctx->global); + bool remove = cmp == TermEquals; + if (UNLIKELY(cmp == TermCompareMemoryAllocFail)) { + error_code = PopcornEtsAllocationFailure; + goto exit; + } + if (remove) { + bool _found = popcorn_ets_hashtable_remove(popcorn_ets_table->hashtable, key, NULL, ctx->global); + UNUSED(_found); + } + } + +exit: + *ret = TRUE_ATOM; + if (LIKELY(popcorn_ets_table != NULL)) { + SMP_UNLOCK(popcorn_ets_table); + } + return error_code; +} + +static bool operation_to_tuple4(term operation, size_t default_pos, term *position, term *increment, term *threshold, term *set_value) +{ + if (term_is_integer(operation)) { + *increment = operation; + *position = term_from_int(default_pos); + *threshold = term_invalid_term(); + *set_value = term_invalid_term(); + return true; + } + + if (!term_is_tuple(operation)) { + return false; + } + int n = term_get_tuple_arity(operation); + if (n != 2 && n != 4) { + return false; + } + + term pos = term_get_tuple_element(operation, 0); + term incr = term_get_tuple_element(operation, 1); + if (!term_is_integer(pos) || !term_is_integer(incr)) { + return false; + } + + if (n == 2) { + *position = pos; + *increment = incr; + *threshold = term_invalid_term(); + *set_value = term_invalid_term(); + return true; + } + + term tresh = term_get_tuple_element(operation, 2); + term set_val = term_get_tuple_element(operation, 3); + if (!term_is_integer(tresh) || !term_is_integer(set_val)) { + return false; + } + + *position = pos; + *increment = incr; + *threshold = tresh; + *set_value = set_val; + return true; +} + +PopcornEtsErrorCode popcorn_ets_update_counter(term ref, term key, term operation, term default_value, term *ret, Context *ctx) +{ + struct PopcornEtsTable *popcorn_ets_table = popcorn_ets_get_table(&ctx->global->popcorn_ets, ctx->process_id, ref, TableAccessWrite); + if (popcorn_ets_table == NULL) { + return PopcornEtsBadAccess; + } + + bool is_duplicate_bag = popcorn_ets_table->table_type == PopcornEtsTableDuplicateBag; + if (is_duplicate_bag) { + SMP_UNLOCK(popcorn_ets_table); + return PopcornEtsBadAccess; + } + + term to_insert = term_invalid_term(); + term list = term_invalid_term(); + PopcornEtsErrorCode result = popcorn_ets_lookup_internal(popcorn_ets_table, key, ETS_NO_INDEX, &list, ctx); + if (result != PopcornEtsOk) { + SMP_UNLOCK(popcorn_ets_table); + return result; + } + if (term_is_nil(list)) { + if (term_is_invalid_term(default_value)) { + SMP_UNLOCK(popcorn_ets_table); + return PopcornEtsBadEntry; + } + to_insert = default_value; + } else { + to_insert = term_get_list_head(list); + } + + if (!(term_is_tuple(to_insert))) { + SMP_UNLOCK(popcorn_ets_table); + return PopcornEtsBadEntry; + } + term position_term, increment_term, threshold_term, set_value_term; + + // +1 to position, +1 to elem after key + size_t default_pos = (popcorn_ets_table->key_index + 1) + 1; + if (!operation_to_tuple4(operation, default_pos, &position_term, &increment_term, &threshold_term, &set_value_term)) { + SMP_UNLOCK(popcorn_ets_table); + return PopcornEtsBadEntry; + } + int arity = term_get_tuple_arity(to_insert); + avm_int_t position = term_to_int(position_term); + if (position < 0) { + SMP_UNLOCK(popcorn_ets_table); + return PopcornEtsBadEntry; + } + size_t index = position - 1; + if (index >= (size_t) arity) { + SMP_UNLOCK(popcorn_ets_table); + return PopcornEtsBadEntry; + } + + term elem = term_get_tuple_element(to_insert, index); + if (!term_is_integer(elem)) { + SMP_UNLOCK(popcorn_ets_table); + return PopcornEtsBadEntry; + } + int increment = term_to_int(increment_term); + // We don't check overflow here. + int elem_value = term_to_int(elem) + increment; + if (!term_is_invalid_term(threshold_term) && !term_is_invalid_term(set_value_term)) { + int threshold = term_to_int(threshold_term); + int set_value = term_to_int(set_value_term); + + if (increment >= 0 && elem_value > threshold) { + elem_value = set_value; + } else if (increment < 0 && elem_value < threshold) { + elem_value = set_value; + } + } + + elem = term_from_int(elem_value); + term_put_tuple_element(to_insert, index, elem); + PopcornEtsErrorCode insert_result = popcorn_ets_insert_internal(popcorn_ets_table, to_insert, NULL, ctx); + if (insert_result == PopcornEtsOk) { + *ret = elem; + } + SMP_UNLOCK(popcorn_ets_table); + return insert_result; +} + +PopcornEtsErrorCode popcorn_ets_update_element(term ref, term key, term value, size_t index, term *ret, Context *ctx) +{ + struct PopcornEtsTable *popcorn_ets_table = popcorn_ets_get_table(&ctx->global->popcorn_ets, ctx->process_id, ref, TableAccessWrite); + if (popcorn_ets_table == NULL) { + return PopcornEtsBadAccess; + } + + bool is_duplicate_bag = popcorn_ets_table->table_type == PopcornEtsTableDuplicateBag; + if (is_duplicate_bag) { + SMP_UNLOCK(popcorn_ets_table); + return PopcornEtsBadAccess; + } + term to_insert = term_invalid_term(); + term list = term_invalid_term(); + PopcornEtsErrorCode result = popcorn_ets_lookup_internal(popcorn_ets_table, key, ETS_NO_INDEX, &list, ctx); + if (result != PopcornEtsOk) { + SMP_UNLOCK(popcorn_ets_table); + return result; + } + if (term_is_nil(list)) { + SMP_UNLOCK(popcorn_ets_table); + *ret = FALSE_ATOM; + return PopcornEtsOk; + } + + to_insert = term_get_list_head(list); + + if (!(term_is_tuple(to_insert))) { + SMP_UNLOCK(popcorn_ets_table); + return PopcornEtsBadEntry; + } + + int arity = term_get_tuple_arity(to_insert); + if (index >= (size_t) arity) { + SMP_UNLOCK(popcorn_ets_table); + return PopcornEtsBadEntry; + } + + term_put_tuple_element(to_insert, index, value); + PopcornEtsErrorCode insert_result = popcorn_ets_insert_internal(popcorn_ets_table, to_insert, NULL, ctx); + SMP_UNLOCK(popcorn_ets_table); + *ret = TRUE_ATOM; + return insert_result; +} + +PopcornEtsErrorCode popcorn_ets_take(term ref, term key, term *ret, Context *ctx) +{ + struct PopcornEtsTable *popcorn_ets_table = popcorn_ets_get_table(&ctx->global->popcorn_ets, ctx->process_id, ref, TableAccessWrite); + if (popcorn_ets_table == NULL) { + return PopcornEtsBadAccess; + } + + struct PopcornEtsHashTableEntry deleted; + bool found = popcorn_ets_hashtable_remove(popcorn_ets_table->hashtable, key, &deleted, ctx->global); + // we can unlock here because hashtable doesn't have reference to the entry and its heap anymore + SMP_UNLOCK(popcorn_ets_table); + + if (!found) { + *ret = term_nil(); + return PopcornEtsOk; + } + + bool is_duplicate_bag = popcorn_ets_table->table_type == PopcornEtsTableDuplicateBag; + size_t size = 0; + if (is_duplicate_bag) { + size = 2 * memory_estimate_usage(deleted.entry); + } else { + size = memory_estimate_usage(deleted.entry) + CONS_SIZE; + } + // we don't need to preserve tuples, they live on different heap + if (UNLIKELY(memory_ensure_free_opt(ctx, size, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + return PopcornEtsAllocationFailure; + } + term entry_or_entries = memory_copy_term_tree(&ctx->heap, deleted.entry); + memory_destroy_heap(deleted.heap, ctx->global); + + if (is_duplicate_bag) { + term taken = term_nil(); + + // return in insertion order + while (!term_is_nil(entry_or_entries)) { + term entry = term_get_list_head(entry_or_entries); + taken = term_list_prepend(entry, taken, &ctx->heap); + entry_or_entries = term_get_list_tail(entry_or_entries); + } + *ret = taken; + } else { + *ret = term_list_prepend(entry_or_entries, term_nil(), &ctx->heap); + } + + return PopcornEtsOk; +} diff --git a/src/libAtomVM/popcorn/popcorn_ets.h b/src/libAtomVM/popcorn/popcorn_ets.h new file mode 100644 index 000000000..22bcb4ab0 --- /dev/null +++ b/src/libAtomVM/popcorn/popcorn_ets.h @@ -0,0 +1,90 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2024 Fred Dushin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later + */ + +#ifndef _POPCORN_ETS_H_ +#define _POPCORN_ETS_H_ + +struct Context; +struct GlobalContext; + +#include "list.h" +#include "synclist.h" +#include "term.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// N.B. Only PopcornEtsTableSet currently supported +typedef enum PopcornEtsTableType +{ + PopcornEtsTableSet, + PopcornEtsTableOrderedSet, + PopcornEtsTableBag, + PopcornEtsTableDuplicateBag +} PopcornEtsTableType; + +typedef enum PopcornEtsAccessType +{ + PopcornEtsAccessPrivate, + PopcornEtsAccessProtected, + PopcornEtsAccessPublic +} PopcornEtsAccessType; + +typedef enum PopcornEtsErrorCode +{ + PopcornEtsOk, + PopcornEtsBadAccess, + PopcornEtsTableNameInUse, + PopcornEtsBadEntry, + PopcornEtsAllocationFailure, + PopcornEtsEntryNotFound, + PopcornEtsBadPosition +} PopcornEtsErrorCode; +struct PopcornEts +{ + // TODO Using a list imposes O(len(popcorn_ets_tables)) cost + // on lookup, so in the future we may want to consider + // a table or map instead of a list. + struct SyncList popcorn_ets_tables; +}; + +void popcorn_ets_init(struct PopcornEts *popcorn_ets); +void popcorn_ets_destroy(struct PopcornEts *popcorn_ets, GlobalContext *global); + +PopcornEtsErrorCode popcorn_ets_create_table(term name, bool is_named, PopcornEtsTableType table_type, PopcornEtsAccessType access_type, size_t keypos, term *ret, Context *ctx); +void popcorn_ets_delete_owned_tables(struct PopcornEts *popcorn_ets, int32_t process_id, GlobalContext *global); + +PopcornEtsErrorCode popcorn_ets_insert(term ref, term entry, bool *entry_inserted, Context *ctx); +PopcornEtsErrorCode popcorn_ets_lookup(term ref, term key, term *ret, Context *ctx); +PopcornEtsErrorCode popcorn_ets_lookup_element(term ref, term key, size_t pos, term *ret, Context *ctx); +PopcornEtsErrorCode popcorn_ets_delete(term ref, term key, term *ret, Context *ctx); +PopcornEtsErrorCode popcorn_ets_drop_table(term ref, term *ret, Context *ctx); +PopcornEtsErrorCode popcorn_ets_update_counter(term ref, term key, term operation, term default_value, term *ret, Context *ctx); +PopcornEtsErrorCode popcorn_ets_delete_object(term ref, term tuple, term *ret, Context *ctx); +PopcornEtsErrorCode popcorn_ets_update_element(term ref, term key, term value, term pos, term *ret, Context *ctx); +PopcornEtsErrorCode popcorn_ets_take(term ref, term key, term *ret, Context *ctx); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/libAtomVM/popcorn/popcorn_ets_hashtable.c b/src/libAtomVM/popcorn/popcorn_ets_hashtable.c new file mode 100644 index 000000000..a468c79d5 --- /dev/null +++ b/src/libAtomVM/popcorn/popcorn_ets_hashtable.c @@ -0,0 +1,333 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2024 Fred Dushin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later + */ + +#include "popcorn_ets_hashtable.h" + +#include "smp.h" +#include "term.h" +#include "utils.h" + +#include +#include + +// #define TRACE_ENABLED +#include "trace.h" + +struct HNode +{ + struct HNode *next; + term key; + term entry; + Heap *heap; +}; + +static uint32_t hash_term(term t, GlobalContext *global); + +struct PopcornEtsHashTable *popcorn_ets_hashtable_new() +{ + struct PopcornEtsHashTable *htable = malloc(sizeof(struct PopcornEtsHashTable)); + if (IS_NULL_PTR(htable)) { + return NULL; + } + + memset(htable->buckets, 0, NUM_BUCKETS * sizeof(struct HNode *)); + htable->capacity = NUM_BUCKETS; + + return htable; +} + +void popcorn_ets_hashtable_destroy(struct PopcornEtsHashTable *hash_table, GlobalContext *global) +{ + for (size_t i = 0; i < hash_table->capacity; ++i) { + struct HNode *node = hash_table->buckets[i]; + while (node != 0) { + memory_destroy_heap(node->heap, global); + struct HNode *next_node = node->next; + free(node); + node = next_node; + } + } +} + +#ifdef TRACE_ENABLED +static void print_info(struct PopcornEtsHashTable *hash_table) +{ + fprintf(stderr, "============\n"); + for (size_t i = 0; i < hash_table->capacity; ++i) { + size_t len = 0; + struct HNode *node = hash_table->buckets[i]; + while (node) { + node = node->next; + ++len; + } + fprintf(stderr, "len bucket[%zu]: %zu\n", i, len); + } +} +#endif + +PopcornEtsHashtableErrorCode popcorn_ets_hashtable_insert(struct PopcornEtsHashTable *hash_table, term key, term entry, PopcornEtsHashtableOptions opts, Heap *entry_heap, GlobalContext *global) +{ + uint32_t hash = hash_term(key, global); + uint32_t index = hash % hash_table->capacity; + +#ifdef TRACE_ENABLED + fprintf(stderr, "hash=%u index=%i key=", hash, index); + term_fprint(stderr, key, global); + fprintf(stderr, "\n"); +#endif + + struct HNode *node = hash_table->buckets[index]; + struct HNode *last_node = NULL; + while (node) { + if (term_compare(key, node->key, TermCompareExact, global) == TermEquals) { + if (opts & PopcornEtsHashtableAllowOverwrite) { + node->key = key; + node->entry = entry; + memory_destroy_heap(node->heap, global); + node->heap = entry_heap; + return PopcornEtsHashtableOk; + } else { + return PopcornEtsHashtableKeyAlreadyExists; + } + } + last_node = node; + node = node->next; + } + + struct HNode *new_node = malloc(sizeof(struct HNode)); + if (IS_NULL_PTR(new_node)) { + return PopcornEtsHashtableError; + } + + new_node->next = NULL; + new_node->key = key; + new_node->entry = entry; + new_node->heap = entry_heap; + + if (last_node) { + last_node->next = new_node; + } else { + hash_table->buckets[index] = new_node; + } + +#ifdef TRACE_ENABLED + print_info(hash_table); +#endif + + return PopcornEtsHashtableOk; +} + +term popcorn_ets_hashtable_lookup(struct PopcornEtsHashTable *hash_table, term key, GlobalContext *global) +{ + uint32_t hash = hash_term(key, global); + uint32_t index = hash % hash_table->capacity; + + const struct HNode *node = hash_table->buckets[index]; + while (node) { + if (term_compare(node->key, key, TermCompareExact, global) == TermEquals) { + return node->entry; + } + node = node->next; + } + + return term_nil(); +} + +bool popcorn_ets_hashtable_remove(struct PopcornEtsHashTable *hash_table, term key, struct PopcornEtsHashTableEntry *removed, GlobalContext *global) +{ + uint32_t hash = hash_term(key, global); + uint32_t index = hash % hash_table->capacity; + + struct HNode *node = hash_table->buckets[index]; + struct HNode *prev_node = NULL; + while (node) { + if (term_compare(node->key, key, TermCompareExact, global) == TermEquals) { + struct HNode *next_node = node->next; + if (prev_node != NULL) { + prev_node->next = next_node; + } else { + hash_table->buckets[index] = next_node; + } + break; + } else { + prev_node = node; + node = node->next; + } + } + + bool found = node != NULL; + bool return_removed = removed != NULL; + if (found && return_removed) { + removed->key = node->key; + removed->entry = node->entry; + removed->heap = node->heap; + } else if (found) { + memory_destroy_heap(node->heap, global); + } + free(node); + return found; +} + +// +// hash function +// +// Conceptually similar to (but not identical to) the `make_hash` algorithm described in +// https://github.com/erlang/otp/blob/cbd1378ee1fde835e55614bac9290b281bafe49a/erts/emulator/beam/utils.c#L644 +// +// Also described in character folding algorithm (PJW Hash) +// https://en.wikipedia.org/wiki/Hash_function#Character_folding +// +// TODO: implement erlang:phash2 using the OTP algorithm +// + +// some large (close to 2^24) primes taken from +// http://compoasso.free.fr/primelistweb/page/prime/liste_online_en.php + +#define LARGE_PRIME_INITIAL 16777259 +#define LARGE_PRIME_ATOM 16777643 +#define LARGE_PRIME_INTEGER 16777781 +#define LARGE_PRIME_FLOAT 16777973 +#define LARGE_PRIME_PID 16778147 +#define LARGE_PRIME_REF 16778441 +#define LARGE_PRIME_BINARY 16780483 +#define LARGE_PRIME_TUPLE 16778821 +#define LARGE_PRIME_LIST 16779179 +#define LARGE_PRIME_MAP 16779449 + +static uint32_t hash_atom(term t, int32_t h, GlobalContext *global) +{ + size_t len; + const uint8_t *data = atom_table_get_atom_string(global->atom_table, term_to_atom_index(t), &len); + for (size_t i = 0; i < len; ++i) { + h = h * LARGE_PRIME_ATOM + data[i]; + } + return h * LARGE_PRIME_ATOM; +} + +static uint32_t hash_integer(term t, int32_t h, GlobalContext *global) +{ + UNUSED(global); + uint64_t n = (uint64_t) term_maybe_unbox_int64(t); + while (n) { + h = h * LARGE_PRIME_INTEGER + (n & 0xFF); + n >>= 8; + } + return h * LARGE_PRIME_INTEGER; +} + +static uint32_t hash_float(term t, int32_t h, GlobalContext *global) +{ + UNUSED(global); + avm_float_t f = term_to_float(t); + uint8_t *data = (uint8_t *) &f; + size_t len = sizeof(float); + for (size_t i = 0; i < len; ++i) { + h = h * LARGE_PRIME_FLOAT + data[i]; + } + return h * LARGE_PRIME_FLOAT; +} + +static uint32_t hash_pid(term t, int32_t h, GlobalContext *global) +{ + UNUSED(global); + uint32_t n = (uint32_t) term_to_local_process_id(t); + while (n) { + h = h * LARGE_PRIME_PID + (n & 0xFF); + n >>= 8; + } + return h * LARGE_PRIME_PID; +} + +static uint32_t hash_reference(term t, int32_t h, GlobalContext *global) +{ + UNUSED(global); + uint64_t n = term_to_ref_ticks(t); + while (n) { + h = h * LARGE_PRIME_REF + (n & 0xFF); + n >>= 8; + } + return h * LARGE_PRIME_REF; +} + +static uint32_t hash_binary(term t, int32_t h, GlobalContext *global) +{ + UNUSED(global); + size_t len = (size_t) term_binary_size(t); + uint8_t *data = (uint8_t *) term_binary_data(t); + for (size_t i = 0; i < len; ++i) { + h = h * LARGE_PRIME_BINARY + data[i]; + } + return h * LARGE_PRIME_BINARY; +} + +static uint32_t hash_term_incr(term t, int32_t h, GlobalContext *global) +{ + if (term_is_atom(t)) { + return hash_atom(t, h, global); + } else if (term_is_any_integer(t)) { + return hash_integer(t, h, global); + } else if (term_is_float(t)) { + return hash_float(t, h, global); + } else if (term_is_pid(t)) { + return hash_pid(t, h, global); + } else if (term_is_reference(t)) { + return hash_reference(t, h, global); + } else if (term_is_binary(t)) { + return hash_binary(t, h, global); + } else if (term_is_tuple(t)) { + size_t arity = term_get_tuple_arity(t); + for (size_t i = 0; i < arity; ++i) { + term elt = term_get_tuple_element(t, (int) i); + h = h * LARGE_PRIME_TUPLE + hash_term_incr(elt, h, global); + } + return h * LARGE_PRIME_TUPLE; + } else if (term_is_list(t)) { + while (!term_is_nonempty_list(t)) { + term elt = term_get_list_head(t); + h = h * LARGE_PRIME_LIST + hash_term_incr(elt, h, global); + t = term_get_list_tail(t); + if (term_is_nil(t)) { + h = h * LARGE_PRIME_LIST; + break; + } else if (!term_is_list(t)) { + h = h * LARGE_PRIME_LIST + hash_term_incr(elt, h, global); + break; + } + } + return h * LARGE_PRIME_TUPLE; + } else if (term_is_map(t)) { + size_t size = term_get_map_size(t); + for (size_t i = 0; i < size; ++i) { + term key = term_get_map_key(t, (avm_uint_t) i); + h = h * LARGE_PRIME_MAP + hash_term_incr(key, h, global); + term value = term_get_map_value(t, (avm_uint_t) i); + h = h * LARGE_PRIME_MAP + hash_term_incr(value, h, global); + } + return h * LARGE_PRIME_MAP; + } else { + fprintf(stderr, "hash_term: unsupported term type: %" TERM_X_FMT "\n", t); + return h; + } +} + +static uint32_t hash_term(term t, GlobalContext *global) +{ + return hash_term_incr(t, LARGE_PRIME_INITIAL, global); +} diff --git a/src/libAtomVM/popcorn/popcorn_ets_hashtable.h b/src/libAtomVM/popcorn/popcorn_ets_hashtable.h new file mode 100644 index 000000000..56776f753 --- /dev/null +++ b/src/libAtomVM/popcorn/popcorn_ets_hashtable.h @@ -0,0 +1,70 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2024 Fred Dushin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later + */ + +#ifndef _POPCORN_ETS_HASHTABLE_H_ +#define _POPCORN_ETS_HASHTABLE_H_ + +#include "globalcontext.h" +#include "term.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define NUM_BUCKETS 16 + +struct PopcornEtsHashTableEntry +{ + term key; + term entry; + Heap *heap; +}; + +struct PopcornEtsHashTable +{ + size_t capacity; + struct HNode *buckets[NUM_BUCKETS]; +}; + +typedef enum PopcornEtsHashtableOptions +{ + PopcornEtsHashtableAllowOverwrite = (1 << 0), +} PopcornEtsHashtableOptions; + +typedef enum PopcornEtsHashtableErrorCode +{ + PopcornEtsHashtableOk = 0, + PopcornEtsHashtableKeyAlreadyExists, + PopcornEtsHashtableError +} PopcornEtsHashtableErrorCode; + +struct PopcornEtsHashTable *popcorn_ets_hashtable_new(); +void popcorn_ets_hashtable_destroy(struct PopcornEtsHashTable *hash_table, GlobalContext *global); + +PopcornEtsHashtableErrorCode popcorn_ets_hashtable_insert(struct PopcornEtsHashTable *hash_table, term key, term entry, PopcornEtsHashtableOptions opts, Heap *entry_heap, GlobalContext *global); +term popcorn_ets_hashtable_lookup(struct PopcornEtsHashTable *hash_table, term key, GlobalContext *global); +bool popcorn_ets_hashtable_remove(struct PopcornEtsHashTable *hash_table, term key, struct PopcornEtsHashTableEntry *removed, GlobalContext *global); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/libAtomVM/popcorn/popcorn_md5.c b/src/libAtomVM/popcorn/popcorn_md5.c new file mode 100644 index 000000000..718547eaa --- /dev/null +++ b/src/libAtomVM/popcorn/popcorn_md5.c @@ -0,0 +1,224 @@ +// https://github.com/Zunawe/md5-c/ +/* + * Derived from the RSA Data Security, Inc. MD5 Message-Digest Algorithm + * and modified slightly to be functionally identical but condensed into control structures. + */ + +#include "popcorn_md5.h" + +/* + * Constants defined by the MD5 algorithm + */ +#define A 0x67452301 +#define B 0xefcdab89 +#define C 0x98badcfe +#define D 0x10325476 + +static uint32_t S[] = { 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, + 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, + 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, + 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21 }; + +static uint32_t K[] = { 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, + 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501, + 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, + 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821, + 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, + 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8, + 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, + 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a, + 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, + 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70, + 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05, + 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665, + 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, + 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1, + 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, + 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391 }; + +/* + * Padding used to make the size (in bits) of the input congruent to 448 mod 512 + */ +static uint8_t PADDING[] = { 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + +/* + * Bit-manipulation functions defined by the MD5 algorithm + */ +#define F(X, Y, Z) ((X & Y) | (~X & Z)) +#define G(X, Y, Z) ((X & Z) | (Y & ~Z)) +#define H(X, Y, Z) (X ^ Y ^ Z) +#define I(X, Y, Z) (Y ^ (X | ~Z)) + +/* + * Rotates a 32-bit word left by n bits + */ +uint32_t rotateLeft(uint32_t x, uint32_t n) +{ + return (x << n) | (x >> (32 - n)); +} + +/* + * Initialize a context + */ +void md5Init(MD5Context *ctx) +{ + ctx->size = (uint64_t) 0; + + ctx->buffer[0] = (uint32_t) A; + ctx->buffer[1] = (uint32_t) B; + ctx->buffer[2] = (uint32_t) C; + ctx->buffer[3] = (uint32_t) D; +} + +/* + * Add some amount of input to the context + * + * If the input fills out a block of 512 bits, apply the algorithm (md5Step) + * and save the result in the buffer. Also updates the overall size. + */ +void md5Update(MD5Context *ctx, uint8_t *input_buffer, size_t input_len) +{ + uint32_t input[16]; + unsigned int offset = ctx->size % 64; + ctx->size += (uint64_t) input_len; + + // Copy each byte in input_buffer into the next space in our context input + for (unsigned int i = 0; i < input_len; ++i) { + ctx->input[offset++] = (uint8_t) * (input_buffer + i); + + // If we've filled our context input, copy it into our local array input + // then reset the offset to 0 and fill in a new buffer. + // Every time we fill out a chunk, we run it through the algorithm + // to enable some back and forth between cpu and i/o + if (offset % 64 == 0) { + for (unsigned int j = 0; j < 16; ++j) { + // Convert to little-endian + // The local variable `input` our 512-bit chunk separated into 32-bit words + // we can use in calculations + input[j] = (uint32_t) (ctx->input[(j * 4) + 3]) << 24 | (uint32_t) (ctx->input[(j * 4) + 2]) << 16 | (uint32_t) (ctx->input[(j * 4) + 1]) << 8 | (uint32_t) (ctx->input[(j * 4)]); + } + md5Step(ctx->buffer, input); + offset = 0; + } + } +} + +/* + * Pad the current input to get to 448 bytes, append the size in bits to the very end, + * and save the result of the final iteration into digest. + */ +void md5Finalize(MD5Context *ctx) +{ + uint32_t input[16]; + unsigned int offset = ctx->size % 64; + unsigned int padding_length = offset < 56 ? 56 - offset : (56 + 64) - offset; + + // Fill in the padding and undo the changes to size that resulted from the update + md5Update(ctx, PADDING, padding_length); + ctx->size -= (uint64_t) padding_length; + + // Do a final update (internal to this function) + // Last two 32-bit words are the two halves of the size (converted from bytes to bits) + for (unsigned int j = 0; j < 14; ++j) { + input[j] = (uint32_t) (ctx->input[(j * 4) + 3]) << 24 | (uint32_t) (ctx->input[(j * 4) + 2]) << 16 | (uint32_t) (ctx->input[(j * 4) + 1]) << 8 | (uint32_t) (ctx->input[(j * 4)]); + } + input[14] = (uint32_t) (ctx->size * 8); + input[15] = (uint32_t) ((ctx->size * 8) >> 32); + + md5Step(ctx->buffer, input); + + // Move the result into digest (convert from little-endian) + for (unsigned int i = 0; i < 4; ++i) { + ctx->digest[(i * 4) + 0] = (uint8_t) ((ctx->buffer[i] & 0x000000FF)); + ctx->digest[(i * 4) + 1] = (uint8_t) ((ctx->buffer[i] & 0x0000FF00) >> 8); + ctx->digest[(i * 4) + 2] = (uint8_t) ((ctx->buffer[i] & 0x00FF0000) >> 16); + ctx->digest[(i * 4) + 3] = (uint8_t) ((ctx->buffer[i] & 0xFF000000) >> 24); + } +} + +/* + * Step on 512 bits of input with the main MD5 algorithm. + */ +void md5Step(uint32_t *buffer, uint32_t *input) +{ + uint32_t AA = buffer[0]; + uint32_t BB = buffer[1]; + uint32_t CC = buffer[2]; + uint32_t DD = buffer[3]; + + uint32_t E; + + unsigned int j; + + for (unsigned int i = 0; i < 64; ++i) { + switch (i / 16) { + case 0: + E = F(BB, CC, DD); + j = i; + break; + case 1: + E = G(BB, CC, DD); + j = ((i * 5) + 1) % 16; + break; + case 2: + E = H(BB, CC, DD); + j = ((i * 3) + 5) % 16; + break; + default: + E = I(BB, CC, DD); + j = (i * 7) % 16; + break; + } + + uint32_t temp = DD; + DD = CC; + CC = BB; + BB = BB + rotateLeft(AA + E + K[i] + input[j], S[i]); + AA = temp; + } + + buffer[0] += AA; + buffer[1] += BB; + buffer[2] += CC; + buffer[3] += DD; +} + +/* + * Functions that run the algorithm on the provided input and put the digest into result. + * result should be able to store 16 bytes. + */ +void md5String(char *input, uint8_t *result) +{ + MD5Context ctx; + md5Init(&ctx); + md5Update(&ctx, (uint8_t *) input, strlen(input)); + md5Finalize(&ctx); + + memcpy(result, ctx.digest, 16); +} + +void md5File(FILE *file, uint8_t *result) +{ + char *input_buffer = malloc(1024); + size_t input_size = 0; + + MD5Context ctx; + md5Init(&ctx); + + while ((input_size = fread(input_buffer, 1, 1024, file)) > 0) { + md5Update(&ctx, (uint8_t *) input_buffer, input_size); + } + + md5Finalize(&ctx); + + free(input_buffer); + + memcpy(result, ctx.digest, 16); +} diff --git a/src/libAtomVM/popcorn/popcorn_md5.h b/src/libAtomVM/popcorn/popcorn_md5.h new file mode 100644 index 000000000..70c21cc23 --- /dev/null +++ b/src/libAtomVM/popcorn/popcorn_md5.h @@ -0,0 +1,26 @@ +// https://github.com/Zunawe/md5-c/ +#ifndef MD5_H +#define MD5_H + +#include +#include +#include +#include + +typedef struct +{ + uint64_t size; // Size of input in bytes + uint32_t buffer[4]; // Current accumulation of hash + uint8_t input[64]; // Input to be used in the next step + uint8_t digest[16]; // Result of algorithm +} MD5Context; + +void md5Init(MD5Context *ctx); +void md5Update(MD5Context *ctx, uint8_t *input, size_t input_len); +void md5Finalize(MD5Context *ctx); +void md5Step(uint32_t *buffer, uint32_t *input); + +void md5String(char *input, uint8_t *result); +void md5File(FILE *file, uint8_t *result); + +#endif diff --git a/src/libAtomVM/popcorn/popcorn_nifs.c b/src/libAtomVM/popcorn/popcorn_nifs.c new file mode 100644 index 000000000..ea1940076 --- /dev/null +++ b/src/libAtomVM/popcorn/popcorn_nifs.c @@ -0,0 +1,903 @@ +// See popcorn_nifs.gperf for status of each NIF + +#include "popcorn_nifs.h" +#include "popcorn_ets.h" +#include "popcorn_md5.h" + +#include "nifs.h" + +#include "atom_table.h" +#include "avmpack.h" +#include "bif.h" +#include "bitstring.h" +#include "context.h" +#include "defaultatoms.h" +#include "dictionary.h" +#include "dist_nifs.h" +#include "erl_nif_priv.h" +#include "externalterm.h" +#include "globalcontext.h" +#include "interop.h" +#include "mailbox.h" +#include "memory.h" +#include "module.h" +#include "platform_nifs.h" +#include "port.h" +#include "posix_nifs.h" +#include "scheduler.h" +#include "synclist.h" +#include "sys.h" +#include "term.h" +#include "term_typedef.h" +#include "unicode.h" +#include "utils.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static term iolist_to_buffer(term list, char **buf, size_t *size) +{ + *buf = NULL; + *size = 0; + + size_t bin_size; + switch (interop_iolist_size(list, &bin_size)) { + case InteropOk: + break; + case InteropMemoryAllocFail: + return OUT_OF_MEMORY_ATOM; + case InteropBadArg: + return BADARG_ATOM; + } + + if (bin_size == 0) { + return OK_ATOM; + } + + char *bin_buf = NULL; + bin_buf = malloc(bin_size * sizeof(char)); + if (IS_NULL_PTR(bin_buf)) { + return OUT_OF_MEMORY_ATOM; + } + + switch (interop_write_iolist(list, bin_buf)) { + case InteropOk: + break; + case InteropMemoryAllocFail: + free(bin_buf); + return OUT_OF_MEMORY_ATOM; + case InteropBadArg: + free(bin_buf); + return BADARG_ATOM; + } + + *buf = bin_buf; + *size = bin_size; + return OK_ATOM; +} + +static term nif_erlang_list_to_bitstring_1(Context *ctx, int argc, term argv[]) +{ + // TODO: this is a copy-pasted erlang_list_to_bitstring + // we shouldimplement proper list_to_bitstring function when the bitstrings are supported + UNUSED(argc); + + term t = argv[0]; + VALIDATE_VALUE(t, term_is_list); + + char *bin_buf = NULL; + char *alloc_ptr = NULL; + size_t bin_size = 0; + + term status = iolist_to_buffer(t, &bin_buf, &bin_size); + if (UNLIKELY(status != OK_ATOM)) { + RAISE_ERROR(status); + } + bool allocated = bin_size > 0; + if (allocated) { + alloc_ptr = bin_buf; + } else { + bin_buf = ""; + bin_size = 0; + } + + if (UNLIKELY(memory_ensure_free(ctx, term_binary_heap_size(bin_size)) != MEMORY_GC_OK)) { + free(alloc_ptr); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term bin_res = term_from_literal_binary(bin_buf, bin_size, &ctx->heap, ctx->global); + + free(alloc_ptr); + return bin_res; +} + +static const struct Nif list_to_bitstring_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_erlang_list_to_bitstring_1 +}; + +static InteropFunctionResult md5_hash_vendored_fold_fun(term t, void *accum) +{ + MD5Context *ctx = (MD5Context *) accum; + if (term_is_integer(t)) { + avm_int64_t tmp = term_maybe_unbox_int64(t); + if (tmp < 0 || tmp > 255) { + return InteropBadArg; + } + uint8_t val = (uint8_t) tmp; + md5Update(ctx, &val, 1); + } else /* term_is_binary(t) */ { + md5Update(ctx, (uint8_t *) term_binary_data(t), term_binary_size(t)); + } + return InteropOk; +} + +static bool do_md5_hash_vendored(term data, unsigned char *dst) +{ + MD5Context ctx; + md5Init(&ctx); + + InteropFunctionResult result = interop_chardata_fold(data, md5_hash_vendored_fold_fun, NULL, (void *) &ctx); + if (UNLIKELY(result != InteropOk)) { + return false; + } + + md5Finalize(&ctx); + memcpy(dst, ctx.digest, 16); + + return true; +} + +#define MAX_MD_SIZE 64 +static term nif_erlang_md5(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + term data = argv[0]; + + if (!(term_is_binary(data) || term_is_list(data))) { + RAISE_ERROR(BADARG_ATOM) + } + + unsigned char digest[MAX_MD_SIZE]; + size_t digest_len = 16; + + if (UNLIKELY(!do_md5_hash_vendored(data, digest))) { + RAISE_ERROR(BADARG_ATOM) + } + + if (UNLIKELY(memory_ensure_free(ctx, term_binary_heap_size(digest_len)) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + return term_from_literal_binary(digest, digest_len, &ctx->heap, ctx->global); +} + +static const struct Nif md5_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_erlang_md5 +}; + +static term nif_code_get_object_code(Context *ctx, int argc, term argv[]) +{ + UNUSED(ctx); + UNUSED(argc); + UNUSED(argv); + return ERROR_ATOM; +} + +static const struct Nif code_get_object_code_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_code_get_object_code +}; + +static term nif_rand_splitmix64_next(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + VALIDATE_VALUE(argv[0], term_is_any_integer); + uint64_t x = term_maybe_unbox_int64(argv[0]); + // implementation based on https://github.com/erlang/otp/blob/d051172925a5c84b2f21850a188a533f885f201c/lib/stdlib/src/rand.erl#L1629 + uint64_t z = (x += 0x9e3779b97f4a7c15); + z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9; + z = (z ^ (z >> 27)) * 0x94d049bb133111eb; + z = z ^ (z >> 31); + // assume pessimisticly both ints will be boxed + if (UNLIKELY(memory_ensure_free_opt(ctx, TUPLE_SIZE(2) + 2 * BOXED_INT64_SIZE, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term result = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(result, 0, term_make_maybe_boxed_int64(z, &ctx->heap)); + term_put_tuple_element(result, 1, term_make_maybe_boxed_int64(x, &ctx->heap)); + return result; +} + +static const struct Nif rand_splitmix64_next_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_rand_splitmix64_next +}; + +static void get_pattern_data_with_sizes(term pattern_term, const char **pattern_data, size_t *sizes, size_t *shortest_pattern_length) +{ + if (term_is_binary(pattern_term)) { + pattern_data[0] = term_binary_data(pattern_term); + sizes[0] = term_binary_size(pattern_term); + *shortest_pattern_length = sizes[0]; + } + + for (size_t i = 0; term_is_nonempty_list(pattern_term); ++i) { + term head = term_get_list_head(pattern_term); + pattern_data[i] = term_binary_data(head); + sizes[i] = term_binary_size(head); + if (i == 0 || sizes[i] < *shortest_pattern_length) { + *shortest_pattern_length = sizes[i]; + } + pattern_term = term_get_list_tail(pattern_term); + } +} + +static const char *find_pattern(const char *bin, size_t bin_size, const char **patterns, const size_t *pattern_sizes, size_t patterns_len, int *matched_pattern_index) +{ + for (size_t i = 0; i < bin_size; i++) { + for (size_t pattern_i = 0; pattern_i < patterns_len; pattern_i++) { + if (pattern_sizes[pattern_i] <= bin_size - i) { + if (memcmp(bin + i, patterns[pattern_i], pattern_sizes[pattern_i]) == 0) { + *matched_pattern_index = pattern_i; + return bin + i; + } + } + } + } + return NULL; +} + +term trim_list(Context *ctx, term list, size_t heap_size, bool trim, bool trim_all) +{ + int proper; + size_t length = term_list_length(list, &proper); + UNUSED(proper); + term *cons = malloc(length * sizeof(term)); + if (IS_NULL_PTR(cons)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + if (UNLIKELY(memory_ensure_free_with_roots(ctx, heap_size, 1, &list, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + free(cons); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + term iter = list; + + for (size_t i = 0; i < length; ++i) { + cons[i] = iter; + iter = term_get_list_tail(iter); + } + + bool found_non_empty = false; + term trimmed = term_nil(); + for (long long i = length - 1; i >= 0; --i) { + term head = term_get_list_head(cons[i]); + + bool is_empty = term_binary_size(head) == 0; + if (is_empty) { + bool trim_tail = trim && !found_non_empty; + if (!trim_tail && !trim_all) { + trimmed = term_list_prepend(head, trimmed, &ctx->heap); + } + } else { + trimmed = term_list_prepend(head, trimmed, &ctx->heap); + found_non_empty = true; + } + } + + free(cons); + return trimmed; +} + +static term nif_binary_split(Context *ctx, int argc, term argv[]) +{ + term bin_term = argv[0]; + term pattern_term = argv[1]; + + VALIDATE_VALUE(bin_term, term_is_binary); + if (!term_is_binary(pattern_term) && !term_is_nonempty_list(pattern_term)) { + RAISE_ERROR(BADARG_ATOM); + } + bool global = false; + bool trim = false; + bool trim_all = false; + + if (argc == 3) { + term options = argv[2]; + if (UNLIKELY(!term_is_list(options))) { + RAISE_ERROR(BADARG_ATOM); + } + term head; + term tail = options; + while (term_is_nonempty_list(tail)) { + head = term_get_list_head(tail); + tail = term_get_list_tail(tail); + switch (head) { + case GLOBAL_ATOM: + global = true; + break; + case TRIM_ATOM: + trim = true; + break; + case TRIM_ALL_ATOM: + trim_all = true; + break; + default: + RAISE_ERROR(BADARG_ATOM); + } + } + } + size_t pattern_list_size = 1; + if (term_is_list(pattern_term)) { + int proper; + pattern_list_size = term_list_length(pattern_term, &proper); + if (UNLIKELY(!proper)) { + RAISE_ERROR(BADARG_ATOM); + } + term iter = pattern_term; + while (term_is_nonempty_list(iter)) { + term head = term_get_list_head(iter); + if (UNLIKELY(term_binary_size(head) == 0)) { + RAISE_ERROR(BADARG_ATOM); + } + iter = term_get_list_tail(iter); + } + } else if (term_is_binary(pattern_term)) { + size_t pattern_size = term_binary_size(pattern_term); + if (UNLIKELY(pattern_size == 0)) { + RAISE_ERROR(BADARG_ATOM); + } + } + + int bin_size = term_binary_size(bin_term); + + const char *bin_data = term_binary_data(bin_term); + const char **pattern_data = malloc(sizeof(char *) * pattern_list_size); + if (IS_NULL_PTR(pattern_data)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + size_t *sizes = malloc(sizeof(size_t) * pattern_list_size); + if (IS_NULL_PTR(sizes)) { + free(pattern_data); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + size_t shortest_pattern_length; + + get_pattern_data_with_sizes(pattern_term, pattern_data, sizes, &shortest_pattern_length); + + // Count segments first to allocate memory once. + size_t num_segments = 1; + const char *temp_bin_data = bin_data; + size_t temp_bin_size = bin_size; + size_t heap_size = 0; + do { + int matched_pattern_index; + const char *found = find_pattern(temp_bin_data, temp_bin_size, pattern_data, sizes, pattern_list_size, &matched_pattern_index); + if (!found) { + break; + } + num_segments++; + heap_size += CONS_SIZE + term_sub_binary_heap_size(argv[0], found - temp_bin_data); + int next_search_offset = found - temp_bin_data + sizes[matched_pattern_index]; + temp_bin_data += next_search_offset; + temp_bin_size -= next_search_offset; + } while (global && temp_bin_size >= shortest_pattern_length); + + heap_size += CONS_SIZE + term_sub_binary_heap_size(argv[0], temp_bin_size); + + term result_list = term_nil(); + + if (num_segments == 1) { + // not found + if (UNLIKELY(memory_ensure_free_with_roots(ctx, 2, 1, argv, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + free(pattern_data); + free(sizes); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + return term_list_prepend(argv[0], result_list, &ctx->heap); + } + + // binary:split/2,3 always return sub binaries, except when copied binaries are as small as sub-binaries. + if (UNLIKELY(memory_ensure_free_with_roots(ctx, heap_size, 2, argv, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + free(pattern_data); + free(sizes); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + // Allocate list first + for (size_t index_segments = 0; index_segments < num_segments; index_segments++) { + result_list = term_list_prepend(term_nil(), result_list, &ctx->heap); + } + + // Reset pointers after allocation + bin_data = term_binary_data(argv[0]); + + pattern_term = argv[1]; + get_pattern_data_with_sizes(pattern_term, pattern_data, sizes, &shortest_pattern_length); + + term list_cursor = result_list; + temp_bin_data = bin_data; + temp_bin_size = bin_size; + term *list_ptr = term_get_list_ptr(list_cursor); + do { + int matched_pattern_index; + const char *found = find_pattern(temp_bin_data, temp_bin_size, pattern_data, sizes, pattern_list_size, &matched_pattern_index); + + if (found) { + term tok = term_maybe_create_sub_binary(argv[0], temp_bin_data - bin_data, found - temp_bin_data, &ctx->heap, ctx->global); + list_ptr[LIST_HEAD_INDEX] = tok; + + list_cursor = list_ptr[LIST_TAIL_INDEX]; + list_ptr = term_get_list_ptr(list_cursor); + + int next_search_offset = found - temp_bin_data + sizes[matched_pattern_index]; + temp_bin_data += next_search_offset; + temp_bin_size -= next_search_offset; + } + + if (!found || !global) { + term rest = term_maybe_create_sub_binary(argv[0], temp_bin_data - bin_data, temp_bin_size, &ctx->heap, ctx->global); + list_ptr[LIST_HEAD_INDEX] = rest; + break; + } + } while (!term_is_nil(list_cursor)); + + free(pattern_data); + free(sizes); + + if (trim || trim_all) { + result_list = trim_list(ctx, result_list, heap_size, trim, trim_all); + } + + return result_list; +} + +static const struct Nif binary_split_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_binary_split +}; + +static term nif_erlang_display_string_2(Context *ctx, int argc, term argv[]) +{ + UNUSED(ctx); + UNUSED(argc); + + FILE *fd; + if (argv[0] == STDOUT_ATOM) { + fd = stdout; + } else if (argv[0] == STDERR_ATOM) { + fd = stderr; + } else { + RAISE_ERROR(BADARG_ATOM); + } + + term t = argv[1]; + if (term_is_nonempty_list(t)) { + int ok; + char *printable = interop_list_to_string(t, &ok); + if (UNLIKELY(!ok)) { + RAISE_ERROR(BADARG_ATOM); + } + + fputs(printable, fd); + free(printable); + } else if (term_is_binary(t)) { + size_t len = term_binary_size(t); + const char *binary_data = term_binary_data(t); + fwrite(binary_data, sizeof(*binary_data), len, fd); + } else if (term_is_nil(t)) { + return TRUE_ATOM; + } else { + RAISE_ERROR(BADARG_ATOM); + } + + return TRUE_ATOM; +} + +static const struct Nif display_string_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_erlang_display_string_2 +}; + +// ETS + +static term nif_ets_new(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + term name = argv[0]; + VALIDATE_VALUE(name, term_is_atom); + + term options = argv[1]; + VALIDATE_VALUE(options, term_is_list); + + term is_named = interop_kv_get_value_default(options, ATOM_STR("\xB", "named_table"), FALSE_ATOM, ctx->global); + term keypos = interop_kv_get_value_default(options, ATOM_STR("\x6", "keypos"), term_from_int(1), ctx->global); + avm_int_t index = term_to_int(keypos) - 1; + + if (UNLIKELY(index < 0)) { + RAISE_ERROR(BADARG_ATOM); + } + + term private = interop_kv_get_value(options, ATOM_STR("\x7", "private"), ctx->global); + term public = interop_kv_get_value(options, ATOM_STR("\x6", "public"), ctx->global); + + PopcornEtsAccessType access = PopcornEtsAccessProtected; + if (!term_is_invalid_term(private)) { + access = PopcornEtsAccessPrivate; + } else if (!term_is_invalid_term(public)) { + access = PopcornEtsAccessPublic; + } + + PopcornEtsTableType type = PopcornEtsTableSet; + term is_duplicate_bag = interop_kv_get_value_default(options, ATOM_STR("\xd", "duplicate_bag"), FALSE_ATOM, ctx->global) == TRUE_ATOM; + if (is_duplicate_bag) { + type = PopcornEtsTableDuplicateBag; + } + + term table = term_invalid_term(); + PopcornEtsErrorCode result = popcorn_ets_create_table(name, is_named == TRUE_ATOM, type, access, (size_t) index, &table, ctx); + switch (result) { + case PopcornEtsOk: + return table; + case PopcornEtsTableNameInUse: + RAISE_ERROR(BADARG_ATOM); + case PopcornEtsAllocationFailure: + RAISE_ERROR(MEMORY_ATOM); + default: + AVM_ABORT(); + } +} + +static inline bool is_popcorn_ets_table_id(term t) +{ + return term_is_reference(t) || term_is_atom(t); +} + +static term nif_ets_insert(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + term ref = argv[0]; + VALIDATE_VALUE(ref, is_popcorn_ets_table_id); + + term entry = argv[1]; + + PopcornEtsErrorCode result = popcorn_ets_insert(ref, entry, NULL, ctx); + switch (result) { + case PopcornEtsOk: + return TRUE_ATOM; + case PopcornEtsBadAccess: + case PopcornEtsBadEntry: + RAISE_ERROR(BADARG_ATOM); + case PopcornEtsAllocationFailure: + RAISE_ERROR(MEMORY_ATOM); + default: + AVM_ABORT(); + } +} + +static term nif_ets_insert_new(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + term ref = argv[0]; + VALIDATE_VALUE(ref, is_popcorn_ets_table_id); + term to_insert = argv[1]; + bool entry_inserted = false; + + PopcornEtsErrorCode result = popcorn_ets_insert(ref, to_insert, &entry_inserted, ctx); + switch (result) { + case PopcornEtsOk: + return entry_inserted ? TRUE_ATOM : FALSE_ATOM; + case PopcornEtsBadAccess: + case PopcornEtsBadEntry: + RAISE_ERROR(BADARG_ATOM); + case PopcornEtsAllocationFailure: + RAISE_ERROR(MEMORY_ATOM); + default: + AVM_ABORT(); + } +} + +static term nif_ets_lookup(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + term ref = argv[0]; + VALIDATE_VALUE(ref, is_popcorn_ets_table_id); + + term key = argv[1]; + + term ret = term_invalid_term(); + PopcornEtsErrorCode result = popcorn_ets_lookup(ref, key, &ret, ctx); + switch (result) { + case PopcornEtsOk: + return ret; + case PopcornEtsBadAccess: + case PopcornEtsBadPosition: + RAISE_ERROR(BADARG_ATOM); + case PopcornEtsAllocationFailure: + RAISE_ERROR(MEMORY_ATOM); + default: + AVM_ABORT(); + } +} + +static term nif_ets_member(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + term ref = argv[0]; + VALIDATE_VALUE(ref, is_popcorn_ets_table_id); + + term key = argv[1]; + + term ret = term_invalid_term(); + PopcornEtsErrorCode result = popcorn_ets_lookup(ref, key, &ret, ctx); + switch (result) { + case PopcornEtsOk: + return term_is_nil(ret) ? FALSE_ATOM : TRUE_ATOM; + case PopcornEtsBadAccess: + RAISE_ERROR(BADARG_ATOM); + case PopcornEtsAllocationFailure: + RAISE_ERROR(MEMORY_ATOM); + default: + AVM_ABORT(); + } +} + +static term nif_ets_take(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + term ref = argv[0]; + VALIDATE_VALUE(ref, is_popcorn_ets_table_id); + + term key = argv[1]; + + term ret = term_invalid_term(); + PopcornEtsErrorCode result = popcorn_ets_take(ref, key, &ret, ctx); + switch (result) { + case PopcornEtsOk: + return ret; + case PopcornEtsBadAccess: + RAISE_ERROR(BADARG_ATOM); + case PopcornEtsAllocationFailure: + RAISE_ERROR(MEMORY_ATOM); + default: + AVM_ABORT(); + } +} + +static term nif_ets_update_counter(Context *ctx, int argc, term argv[]) +{ + term ref = argv[0]; + VALIDATE_VALUE(ref, is_popcorn_ets_table_id); + + term key = argv[1]; + term operation = argv[2]; + term default_value = term_invalid_term(); + if (argc == 4) { + default_value = argv[3]; + VALIDATE_VALUE(default_value, term_is_tuple); + term_put_tuple_element(default_value, 0, key); + } + term ret = term_invalid_term(); + PopcornEtsErrorCode result = popcorn_ets_update_counter(ref, key, operation, default_value, &ret, ctx); + switch (result) { + case PopcornEtsOk: + return ret; + case PopcornEtsBadAccess: + case PopcornEtsBadEntry: + RAISE_ERROR(BADARG_ATOM); + case PopcornEtsAllocationFailure: + RAISE_ERROR(MEMORY_ATOM); + default: + AVM_ABORT(); + } +} + +static term nif_ets_update_element(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + term ref = argv[0]; + VALIDATE_VALUE(ref, is_popcorn_ets_table_id); + + term key = argv[1]; + term operation = argv[2]; + VALIDATE_VALUE(operation, term_is_tuple); + if (term_get_tuple_arity(operation) != 2) { + RAISE_ERROR(BADARG_ATOM); + } + term pos = term_get_tuple_element(operation, 0); + VALIDATE_VALUE(pos, term_is_integer); + term value = term_get_tuple_element(operation, 1); + + avm_int_t index = term_to_int(pos) - 1; + if (UNLIKELY(index < 0)) { + RAISE_ERROR(BADARG_ATOM); + } + + term ret = term_invalid_term(); + PopcornEtsErrorCode result = popcorn_ets_update_element(ref, key, value, (size_t) index, &ret, ctx); + switch (result) { + case PopcornEtsOk: + return ret; + case PopcornEtsBadAccess: + case PopcornEtsBadEntry: + RAISE_ERROR(BADARG_ATOM); + case PopcornEtsAllocationFailure: + RAISE_ERROR(MEMORY_ATOM); + default: + AVM_ABORT(); + } +} + +static term nif_ets_lookup_element(Context *ctx, int argc, term argv[]) +{ + term ref = argv[0]; + VALIDATE_VALUE(ref, is_popcorn_ets_table_id); + + term key = argv[1]; + term pos = argv[2]; + VALIDATE_VALUE(pos, term_is_integer); + avm_int_t index = term_to_int(pos) - 1; + if (UNLIKELY(index < 0)) { + RAISE_ERROR(BADARG_ATOM); + } + + term default_value = term_invalid_term(); + if (argc == 4) { + default_value = argv[3]; + } + + term ret = term_invalid_term(); + PopcornEtsErrorCode result = popcorn_ets_lookup_element(ref, key, (size_t) index, &ret, ctx); + switch (result) { + case PopcornEtsOk: + return ret; + case PopcornEtsEntryNotFound: + if (!term_is_invalid_term(default_value)) { + return default_value; + } + case PopcornEtsBadPosition: + case PopcornEtsBadAccess: + RAISE_ERROR(BADARG_ATOM); + case PopcornEtsAllocationFailure: + RAISE_ERROR(MEMORY_ATOM); + default: + AVM_ABORT(); + } +} + +static term nif_ets_delete(Context *ctx, int argc, term argv[]) +{ + term ref = argv[0]; + VALIDATE_VALUE(ref, is_popcorn_ets_table_id); + term ret = term_invalid_term(); + PopcornEtsErrorCode result; + if (argc == 2) { + term key = argv[1]; + result = popcorn_ets_delete(ref, key, &ret, ctx); + } else { + result = popcorn_ets_drop_table(ref, &ret, ctx); + } + + switch (result) { + case PopcornEtsOk: + return ret; + case PopcornEtsBadAccess: + RAISE_ERROR(BADARG_ATOM); + case PopcornEtsAllocationFailure: + RAISE_ERROR(MEMORY_ATOM); + default: + AVM_ABORT(); + } +} + +static term nif_ets_delete_object(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + term ref = argv[0]; + VALIDATE_VALUE(ref, is_popcorn_ets_table_id); + + term tuple = argv[1]; + VALIDATE_VALUE(tuple, term_is_tuple); + + term ret = term_invalid_term(); + PopcornEtsErrorCode result = popcorn_ets_delete_object(ref, tuple, &ret, ctx); + switch (result) { + case PopcornEtsOk: + return ret; + case PopcornEtsBadAccess: + case PopcornEtsBadPosition: + RAISE_ERROR(BADARG_ATOM); + case PopcornEtsAllocationFailure: + RAISE_ERROR(MEMORY_ATOM); + default: + AVM_ABORT(); + } +} + +static const struct Nif ets_new_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_ets_new +}; + +static const struct Nif ets_insert_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_ets_insert +}; + +static const struct Nif ets_insert_new_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_ets_insert_new +}; + +static const struct Nif ets_update_counter_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_ets_update_counter +}; + +static const struct Nif ets_update_element_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_ets_update_element +}; + +static const struct Nif ets_lookup_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_ets_lookup +}; + +static const struct Nif ets_member_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_ets_member +}; + +static const struct Nif ets_take_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_ets_take +}; + +static const struct Nif ets_lookup_element_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_ets_lookup_element +}; + +static const struct Nif ets_delete_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_ets_delete +}; + +static const struct Nif ets_delete_object_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_ets_delete_object +}; + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmissing-field-initializers" +#pragma GCC diagnostic ignored "-Wunused-parameter" +#include "popcorn_nifs_hash.h" +#pragma GCC diagnostic pop + +const struct Nif *popcorn_nifs_get_nif(const char *mfa) +{ + const NifNameAndNifPtr *nameAndPtr = popcorn_nif_in_word_set(mfa, strlen(mfa)); + if (nameAndPtr) { + return nameAndPtr->nif; + } + return NULL; +} diff --git a/src/libAtomVM/popcorn/popcorn_nifs.gperf b/src/libAtomVM/popcorn/popcorn_nifs.gperf new file mode 100644 index 000000000..999f0b525 --- /dev/null +++ b/src/libAtomVM/popcorn/popcorn_nifs.gperf @@ -0,0 +1,46 @@ +%readonly-tables +%define lookup-function-name popcorn_nif_in_word_set + +%{ +#include +typedef struct NifNameAndNifPtr NifNameAndNifPtr; +%} +struct NifNameAndNifPtr +{ + const char *name; + const struct Nif *nif; +}; +%% +# Waiting to be upstreamed +binary:split/2, &binary_split_nif +binary:split/3, &binary_split_nif +erlang:display_string/2, &display_string_nif +# +# Upstreaming in progress - mock implementation +code:get_object_code/1, &code_get_object_code_nif +# +# Not upstreamable +# +# Hack; could be implemented in AtomVM's Erlang stdlib +erlang:list_to_bitstring/1, &list_to_bitstring_nif +# +avm_rand:splitmix64_next/1, &rand_splitmix64_next_nif +# +erlang:md5/1, &md5_nif +# +# ETS +# +ets:new/2, &ets_new_nif +ets:insert_new/2, &ets_insert_new_nif +ets:update_counter/3, &ets_update_counter_nif +ets:update_counter/4, &ets_update_counter_nif +ets:update_element/3, &ets_update_element_nif +ets:take/2, &ets_take_nif +ets:member/2, &ets_member_nif +ets:insert/2, &ets_insert_nif +ets:lookup/2, &ets_lookup_nif +ets:lookup_element/3, &ets_lookup_element_nif +ets:lookup_element/4, &ets_lookup_element_nif +ets:delete/2, &ets_delete_nif +ets:delete/1, &ets_delete_nif +ets:delete_object/2, &ets_delete_object_nif diff --git a/src/libAtomVM/popcorn/popcorn_nifs.h b/src/libAtomVM/popcorn/popcorn_nifs.h new file mode 100644 index 000000000..1a38a1730 --- /dev/null +++ b/src/libAtomVM/popcorn/popcorn_nifs.h @@ -0,0 +1 @@ +const struct Nif *popcorn_nifs_get_nif(const char *mfa); diff --git a/src/platforms/emscripten/src/CMakeLists.txt b/src/platforms/emscripten/src/CMakeLists.txt index 38da49320..027f47487 100644 --- a/src/platforms/emscripten/src/CMakeLists.txt +++ b/src/platforms/emscripten/src/CMakeLists.txt @@ -28,8 +28,8 @@ add_subdirectory(../../../libAtomVM libAtomVM) target_link_libraries(AtomVM PUBLIC libAtomVM) target_compile_options(libAtomVM PUBLIC -O3 -fno-exceptions -fno-rtti -pthread -sINLINING_LIMIT -sUSE_ZLIB=1) target_compile_definitions(libAtomVM PRIVATE WITH_ZLIB) -target_link_options(AtomVM PRIVATE -sEXPORTED_RUNTIME_METHODS=ccall -sUSE_ZLIB=1 -O3 -pthread -sFETCH -lwebsocket.js --pre-js ${CMAKE_CURRENT_SOURCE_DIR}/atomvm.pre.js) - +target_link_options(AtomVM PRIVATE -sEXPORTED_RUNTIME_METHODS=ccall,cwrap,stringToNewUTF8,FS -sEMULATE_FUNCTION_POINTER_CASTS=1 -sEXPORTED_FUNCTIONS=_malloc,_cast,_call,_next_tracked_object_key,_main -sEXPORT_ES6=1 -sUSE_ZLIB=1 -O3 -pthread -sFETCH -lwebsocket.js --pre-js ${CMAKE_CURRENT_SOURCE_DIR}/atomvm.pre.js) +set(CMAKE_EXECUTABLE_SUFFIX ".mjs") if (CMAKE_BUILD_TYPE STREQUAL "Debug") target_link_options(AtomVM PRIVATE -sASSERTIONS=2 -sSAFE_HEAP -sSTACK_OVERFLOW_CHECK) endif() diff --git a/src/platforms/emscripten/src/atomvm.pre.js b/src/platforms/emscripten/src/atomvm.pre.js index 73ac9f647..af0b15047 100644 --- a/src/platforms/emscripten/src/atomvm.pre.js +++ b/src/platforms/emscripten/src/atomvm.pre.js @@ -17,10 +17,58 @@ * * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later */ -Module['cast'] = function(name, message) { - ccall("cast", 'void', ['string', 'string'], [name, message]); +Module["cast"] = function (name, message) { + ccall("cast", "void", ["string", "string"], [name, message]); }; -Module['call'] = async function(name, message) { - const promiseId = ccall("call", 'integer', ['string', 'string'], [name, message]); - return promiseMap.get(promiseId).promise; +Module["call"] = async function (name, message) { + const promiseId = ccall( + "call", + "integer", + ["string", "string"], + [name, message], + ); + return promiseMap.get(promiseId).promise; }; +Module["nextTrackedObjectKey"] = function () { + return ccall("next_tracked_object_key", "integer", [], []); +}; +Module["trackedObjectsMap"] = new Map(); +Module["onTrackedObjectDelete"] = (key) => { + Module["trackedObjectsMap"].delete(key); +}; +Module["onGetTrackedObjects"] = (keys) => { + const getTrackedObject = (key) => Module["trackedObjectsMap"].get(key); + return keys.map(getTrackedObject); +}; +Module["onRunTrackedJs"] = (scriptString, isDebug) => { + const trackValue = (value) => { + const key = Module["nextTrackedObjectKey"](); + Module["trackedObjectsMap"].set(key, value); + return key; + }; + + let result; + try { + const indirectEval = eval; + result = indirectEval(scriptString); + } catch (_e) { + return null; + } + isDebug && ensureValidResult(result); + return result?.map(trackValue) ?? []; +}; + +function ensureValidResult(result) { + const isIndex = (k) => typeof k === "number"; + + if (result === null) { + return; + } + if (Array.isArray(result) && keys.every(isIndex)) { + return; + } + + const message = + "Evaluated script returned invalid value. Expected number array or null"; + throw new Error(message); +} diff --git a/src/platforms/emscripten/src/lib/emscripten_sys.h b/src/platforms/emscripten/src/lib/emscripten_sys.h index 1889b2ab9..8e396e930 100644 --- a/src/platforms/emscripten/src/lib/emscripten_sys.h +++ b/src/platforms/emscripten/src/lib/emscripten_sys.h @@ -108,14 +108,21 @@ struct EmscriptenMessageUnregisterHTMLEvent struct HTMLEventUserDataResource *rsrc; }; +struct TrackedObjectResource +{ + int32_t key; +}; + struct EmscriptenPlatformData { pthread_mutex_t poll_mutex; pthread_cond_t poll_cond; struct ListHead messages; + atomic_size_t next_tracked_object_key; ErlNifResourceType *promise_resource_type; ErlNifResourceType *htmlevent_user_data_resource_type; ErlNifResourceType *websocket_resource_type; + ErlNifResourceType *tracked_object_resource_type; #ifndef AVM_NO_SMP Mutex *entropy_mutex; @@ -134,6 +141,7 @@ void sys_enqueue_emscripten_cast_message(GlobalContext *glb, const char *target, em_promise_t sys_enqueue_emscripten_call_message(GlobalContext *glb, const char *target, const char *message); void sys_enqueue_emscripten_htmlevent_message(GlobalContext *glb, int32_t target_pid, term message, term user_data, HeapFragment *heap); void sys_enqueue_emscripten_unregister_htmlevent_message(GlobalContext *glb, struct HTMLEventUserDataResource *rsrc); +size_t sys_get_next_tracked_object_key(GlobalContext *glb); void sys_promise_resolve_int_and_destroy(em_promise_t promise, em_promise_result_t result, int value); void sys_promise_resolve_str_and_destroy(em_promise_t promise, em_promise_result_t result, int value); diff --git a/src/platforms/emscripten/src/lib/platform_defaultatoms.def b/src/platforms/emscripten/src/lib/platform_defaultatoms.def index 5ffa6a5bc..9c4c0cb90 100644 --- a/src/platforms/emscripten/src/lib/platform_defaultatoms.def +++ b/src/platforms/emscripten/src/lib/platform_defaultatoms.def @@ -24,3 +24,4 @@ X(WEBSOCKET_ATOM, "\x9", "websocket") X(WEBSOCKET_OPEN_ATOM, "\xE", "websocket_open") X(WEBSOCKET_CLOSE_ATOM, "\xF", "websocket_close") X(WEBSOCKET_ERROR_ATOM, "\xF", "websocket_error") +X(BADVALUE_ATOM, "\x8", "badvalue") diff --git a/src/platforms/emscripten/src/lib/platform_nifs.c b/src/platforms/emscripten/src/lib/platform_nifs.c index ed0a75f87..20443df72 100644 --- a/src/platforms/emscripten/src/lib/platform_nifs.c +++ b/src/platforms/emscripten/src/lib/platform_nifs.c @@ -26,6 +26,7 @@ #include #include #include +#include #include #include @@ -157,6 +158,289 @@ static term nif_emscripten_promise_reject(Context *ctx, int argc, term argv[]) return nif_emscripten_promise_resolve_reject(ctx, argc, argv, EM_PROMISE_REJECT); } +static term term_tracked_object_from_key(Context *ctx, atomic_size_t key) +{ + struct EmscriptenPlatformData *platform = ctx->global->platform_data; + struct TrackedObjectResource *rsrc_obj = enif_alloc_resource(platform->tracked_object_resource_type, sizeof(struct TrackedObjectResource)); + if (IS_NULL_PTR(rsrc_obj)) { + return term_invalid_term(); + } + rsrc_obj->key = key; + term obj = enif_make_resource(erl_nif_env_from_context(ctx), rsrc_obj); + enif_release_resource(rsrc_obj); + return obj; +} + +// clang-format off +EM_JS(uint32_t *, js_tracked_eval, (const char *code, uint32_t *size, bool debug), { + const keys = Module['onRunTrackedJs'](UTF8ToString(code), debug); + const error = keys === null; + if (error) { + HEAPU32[size / HEAPU32.BYTES_PER_ELEMENT] = 0; + return 0; + } + + const ptr = Module['_malloc'](keys.length * HEAPU32.BYTES_PER_ELEMENT); + HEAPU32[size / HEAPU32.BYTES_PER_ELEMENT] = keys.length; + HEAPU32.set(keys, ptr / HEAPU32.BYTES_PER_ELEMENT); + return ptr; +}); +// clang-format on + +static void do_run_script_tracked(const char *script, int32_t sync_caller_pid, GlobalContext *global) +{ +#ifdef NDEBUG + bool debug = false; +#else + bool debug = true; +#endif + uint32_t keys_n; + uint32_t *keys = js_tracked_eval(script, &keys_n, debug); + Context *target_ctx = globalcontext_get_process_lock(global, sync_caller_pid); + if (target_ctx) { + term result = term_invalid_term(); + term refs = term_nil(); + if (UNLIKELY(memory_ensure_free_opt(target_ctx, TUPLE_SIZE(2) + LIST_SIZE(keys_n, TERM_BOXED_REFC_BINARY_SIZE), MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + // TODO: how to raise? + result = OUT_OF_MEMORY_ATOM; + goto send_result; + } + result = term_alloc_tuple(2, &target_ctx->heap); + + if (IS_NULL_PTR(keys)) { + term_put_tuple_element(result, 0, ERROR_ATOM); + term_put_tuple_element(result, 1, BADARG_ATOM); + goto send_result; + } + + if (keys_n == 0) { + term_put_tuple_element(result, 0, OK_ATOM); + term_put_tuple_element(result, 1, term_nil()); + goto send_result; + } + + for (long i = keys_n - 1; i >= 0; --i) { + term tracked_object = term_tracked_object_from_key(target_ctx, keys[i]); + // we can't easily recover from OOM here + assert(!term_is_invalid_term(tracked_object)); + refs = term_list_prepend(tracked_object, refs, &target_ctx->heap); + } + term_put_tuple_element(result, 0, OK_ATOM); + term_put_tuple_element(result, 1, refs); + + send_result: + free(keys); + mailbox_send_term_signal(target_ctx, TrapAnswerSignal, result); + globalcontext_get_process_unlock(global, target_ctx); + } else { + // sender died + free(keys); + } +} + +static term nif_emscripten_run_script_tracked(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + term script_term = argv[0]; + + int ok; + char *script = interop_term_to_string(script_term, &ok); + if (UNLIKELY(!ok)) { + RAISE_ERROR(BADARG_ATOM); + } + // Trap caller waiting for completion + context_update_flags(ctx, ~NoFlags, Trap); + // script will be freed as it's passed as satellite + emscripten_dispatch_to_thread(emscripten_main_runtime_thread_id(), EM_FUNC_SIG_VIII, do_run_script_tracked, script, script, ctx->process_id, ctx->global); + return term_invalid_term(); +} + +// clang-format off +EM_JS(char *, js_get_tracked_objects, (uint32_t *keys_ptr, uint32_t keys_n, uint32_t **sizes, uint8_t **statuses, uint32_t *objects_n, uint32_t *strings_n, uint32_t *all_byte_size), { + const OK = 0; + const BAD_KEY = 1; + const NOT_STRING = 2; + + const keysOffset = keys_ptr / HEAPU32.BYTES_PER_ELEMENT; + const keys = [...HEAPU32.subarray(keysOffset, keysOffset + keys_n)]; + + const objects = Module['onGetTrackedObjects'](keys); + if (!Array.isArray(objects)) { + return 0; + } + const n = objects.length; + + if (n === 0) { + return 0; + } + + const sizesPtr = Module['_malloc'](n * HEAPU32.BYTES_PER_ELEMENT); + const statusPtr = Module['_malloc'](n * HEAPU8.BYTES_PER_ELEMENT); + let allByteSize = 0; + let stringsN = 0; + + for (let i = 0; i < n; ++i) { + let status = OK; + let byteSize = 0; + const object = objects[i]; + if (object === undefined) { + status = BAD_KEY; + } else if (typeof object !== "string") { + status = NOT_STRING; + } else { + byteSize = lengthBytesUTF8(object); + stringsN += 1; + } + HEAPU8[statusPtr / HEAPU8.BYTES_PER_ELEMENT + i] = status; + HEAPU32[sizesPtr / HEAPU32.BYTES_PER_ELEMENT + i] = byteSize; + allByteSize += byteSize; + } + + const stringsPtr = Module['_malloc'](allByteSize * HEAPU8.BYTES_PER_ELEMENT); + let currentStringsPtr = stringsPtr; + for (let i = 0; i < n; ++i) { + const status = HEAPU8[statusPtr / HEAPU8.BYTES_PER_ELEMENT + i]; + if (status !== OK) { + continue; + } + const string = objects[i]; + const size = HEAPU32[sizesPtr / HEAPU32.BYTES_PER_ELEMENT + i]; + // stringToUTF8 includes null byte which we don't need + stringToUTF8(string, currentStringsPtr, size+1); + currentStringsPtr += size; + } + + HEAPU32[statuses / HEAPU32.BYTES_PER_ELEMENT] = statusPtr; + HEAPU32[sizes / HEAPU32.BYTES_PER_ELEMENT] = sizesPtr; + HEAPU32[objects_n / HEAPU32.BYTES_PER_ELEMENT] = n; + HEAPU32[strings_n / HEAPU32.BYTES_PER_ELEMENT] = stringsN; + HEAPU32[all_byte_size / HEAPU32.BYTES_PER_ELEMENT] = allByteSize; + + return stringsPtr; +}); +// clang-format on + +static void do_get_tracked_objects(uint32_t *ref_keys, size_t keys_n, int32_t sync_caller_pid, GlobalContext *global) +{ + static const uint8_t OK = 0; + static const uint8_t BAD_KEY = 1; + static const uint8_t NOT_STRING = 2; + UNUSED(BAD_KEY); + + uint32_t objects_n = 0; + uint32_t strings_n = 0; + uint32_t all_byte_size = 0; + uint32_t *sizes = NULL; + uint8_t *statuses = NULL; + + char *strings = js_get_tracked_objects(ref_keys, keys_n, &sizes, &statuses, &objects_n, &strings_n, &all_byte_size); + assert(strings_n <= objects_n); + Context *target_ctx = globalcontext_get_process_lock(global, sync_caller_pid); + if (target_ctx) { + term result = term_invalid_term(); + if (IS_NULL_PTR(strings)) { + result = BADVALUE_ATOM; + goto send_result; + } + size_t size = LIST_SIZE(objects_n, TUPLE_SIZE(2)) + LIST_SIZE(strings_n, BINARY_HEADER_SIZE) + term_binary_data_size_in_terms(all_byte_size); + if (UNLIKELY(memory_ensure_free_opt(target_ctx, size, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + result = OUT_OF_MEMORY_ATOM; + goto send_result; + } + + // move pointer to one byte past buffer + const char *current_string = strings + all_byte_size; + result = term_nil(); + for (long i = objects_n - 1; i >= 0; --i) { + uint8_t status = statuses[i]; + + term tuple = term_alloc_tuple(2, &target_ctx->heap); + if (status == OK) { + size_t size = sizes[i]; + current_string -= size; + + term binary = term_create_uninitialized_binary(size, &target_ctx->heap, global); + char *data = (char *) term_binary_data(binary); + memcpy(data, current_string, size); + + term_put_tuple_element(tuple, 0, OK_ATOM); + term_put_tuple_element(tuple, 1, binary); + } else if (status == NOT_STRING) { + term_put_tuple_element(tuple, 0, ERROR_ATOM); + term_put_tuple_element(tuple, 1, BADVALUE_ATOM); + } else /* BAD_KEY */ { + term_put_tuple_element(tuple, 0, ERROR_ATOM); + term_put_tuple_element(tuple, 1, BADKEY_ATOM); + } + result = term_list_prepend(tuple, result, &target_ctx->heap); + } + + send_result: + free(sizes); + free(statuses); + free(strings); + mailbox_send_term_signal(target_ctx, TrapAnswerSignal, result); + globalcontext_get_process_unlock(global, target_ctx); + } // else: sender died +} + +static term nif_emscripten_get_tracked(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + term refs = argv[0]; + term type = argv[1]; + struct EmscriptenPlatformData *platform = ctx->global->platform_data; + ErlNifEnv *env = erl_nif_env_from_context(ctx); + + VALIDATE_VALUE(refs, term_is_list); + if (UNLIKELY(type != KEY_ATOM && type != VALUE_ATOM)) { + RAISE_ERROR(BADARG_ATOM); + } + int proper; + size_t n = term_list_length(refs, &proper); + if (UNLIKELY(!proper)) { + RAISE_ERROR(BADARG_ATOM); + } + + if (n == 0) { + RAISE_ERROR(BADARG_ATOM); + } + + int32_t *ref_keys = malloc(n * sizeof(int32_t)); + if (IS_NULL_PTR(ref_keys)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + for (size_t i = 0; i < n; ++i) { + term ref = term_get_list_head(refs); + void *obj; + if (UNLIKELY(!enif_get_resource(env, ref, platform->tracked_object_resource_type, &obj))) { + free(ref_keys); + RAISE_ERROR(BADARG_ATOM); + } + struct TrackedObjectResource *tracked_object_rsrc = (struct TrackedObjectResource *) obj; + ref_keys[i] = tracked_object_rsrc->key; + refs = term_get_list_tail(refs); + } + + if (type == KEY_ATOM) { + if (UNLIKELY(memory_ensure_free_opt(ctx, LIST_SIZE(n, 1), MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + term keys = term_nil(); + for (long i = n - 1; i >= 0; --i) { + keys = term_list_prepend(term_from_int32(ref_keys[i]), keys, &ctx->heap); + } + return keys; + } + assert(type == VALUE_ATOM); + // Trap caller waiting for completion + context_update_flags(ctx, ~NoFlags, Trap); + emscripten_dispatch_to_thread(emscripten_main_runtime_thread_id(), EM_FUNC_SIG_VIIII, do_get_tracked_objects, NULL, ref_keys, n, ctx->process_id, ctx->global); + return term_invalid_term(); +} + static const struct Nif atomvm_platform_nif = { .base.type = NIFFunctionType, .nif_ptr = nif_atomvm_platform @@ -177,6 +461,14 @@ static const struct Nif emscripten_promise_reject_nif = { .base.type = NIFFunctionType, .nif_ptr = nif_emscripten_promise_reject }; +static const struct Nif emscripten_run_script_tracked = { + .base.type = NIFFunctionType, + .nif_ptr = nif_emscripten_run_script_tracked +}; +static const struct Nif emscripten_get_tracked = { + .base.type = NIFFunctionType, + .nif_ptr = nif_emscripten_get_tracked, +}; static bool get_callback_target(Context *ctx, term t, const char **target, char **str) { @@ -788,6 +1080,12 @@ const struct Nif *platform_nifs_get_nif(const char *nifname) if (strcmp("run_script/2", nifname) == 0) { return &emscripten_run_script_nif; } + if (strcmp("run_script_tracked/1", nifname) == 0) { + return &emscripten_run_script_tracked; + } + if (strcmp("get_tracked/2", nifname) == 0) { + return &emscripten_get_tracked; + } if (strcmp("promise_resolve/1", nifname) == 0) { return &emscripten_promise_resolve_nif; } diff --git a/src/platforms/emscripten/src/lib/sys.c b/src/platforms/emscripten/src/lib/sys.c index ab8c0404b..47cf5e0de 100644 --- a/src/platforms/emscripten/src/lib/sys.c +++ b/src/platforms/emscripten/src/lib/sys.c @@ -48,6 +48,12 @@ #include "platform_defaultatoms.h" #include "websocket_nifs.h" +size_t sys_get_next_tracked_object_key(GlobalContext *glb) +{ + struct EmscriptenPlatformData *platform = glb->platform_data; + return platform->next_tracked_object_key++; +} + /** * @brief resolve a promise with an int value and destroy it * @details called on the main thread using `emscripten_dispatch_to_thread` @@ -58,15 +64,9 @@ void sys_promise_resolve_int_and_destroy(em_promise_t promise, em_promise_result_t result, int value) { if (result == EM_PROMISE_FULFILL) { - EM_ASM({ - promiseMap.get($0).resolve($1); - }, - promise, value); + EM_ASM({ promiseMap.get($0).resolve($1); }, promise, value); } else { - EM_ASM({ - promiseMap.get($0).reject($1); - }, - promise, value); + EM_ASM({ promiseMap.get($0).reject($1); }, promise, value); } emscripten_promise_destroy(promise); } @@ -81,15 +81,9 @@ void sys_promise_resolve_int_and_destroy(em_promise_t promise, em_promise_result void sys_promise_resolve_str_and_destroy(em_promise_t promise, em_promise_result_t result, int value) { if (result == EM_PROMISE_FULFILL) { - EM_ASM({ - promiseMap.get($0).resolve(UTF8ToString($1)); - }, - promise, value); + EM_ASM({ promiseMap.get($0).resolve(UTF8ToString($1)); }, promise, value); } else { - EM_ASM({ - promiseMap.get($0).reject(UTF8ToString($1)); - }, - promise, value); + EM_ASM({ promiseMap.get($0).reject(UTF8ToString($1)); }, promise, value); } emscripten_promise_destroy(promise); } @@ -130,6 +124,19 @@ static void htmlevent_user_data_down(ErlNifEnv *caller_env, void *obj, ErlNifPid } } +static void do_remove_tracked_object(atomic_size_t key) +{ + EM_ASM({ Module['onTrackedObjectDelete']($0); }, key); +} + +static void tracked_object_dtor(ErlNifEnv *caller_env, void *obj) +{ + UNUSED(caller_env); + + struct TrackedObjectResource *tracked_object_rsrc = (struct TrackedObjectResource *) obj; + emscripten_dispatch_to_thread(emscripten_main_runtime_thread_id(), EM_FUNC_SIG_VI, do_remove_tracked_object, NULL, tracked_object_rsrc->key); +} + static const ErlNifResourceTypeInit promise_resource_type_init = { .members = 1, .dtor = promise_dtor, @@ -142,6 +149,11 @@ static const ErlNifResourceTypeInit htmlevent_user_data_resource_type_init = { .down = htmlevent_user_data_down, }; +static const ErlNifResourceTypeInit tracked_object_resource_type_init = { + .members = 1, + .dtor = tracked_object_dtor +}; + void sys_init_platform(GlobalContext *glb) { struct EmscriptenPlatformData *platform = malloc(sizeof(struct EmscriptenPlatformData)); @@ -158,24 +170,34 @@ void sys_init_platform(GlobalContext *glb) AVM_ABORT(); } list_init(&platform->messages); + platform->next_tracked_object_key = 0; ErlNifEnv env; erl_nif_env_partial_init_from_globalcontext(&env, glb); + platform->promise_resource_type = enif_init_resource_type(&env, "promise", &promise_resource_type_init, ERL_NIF_RT_CREATE, NULL); if (IS_NULL_PTR(platform->promise_resource_type)) { fprintf(stderr, "Cannot initialize promise_resource_type"); AVM_ABORT(); } + platform->htmlevent_user_data_resource_type = enif_init_resource_type(&env, "htmlevent_user_data", &htmlevent_user_data_resource_type_init, ERL_NIF_RT_CREATE, NULL); if (IS_NULL_PTR(platform->htmlevent_user_data_resource_type)) { fprintf(stderr, "Cannot initialize htmlevent_user_data_resource_type"); AVM_ABORT(); } + platform->websocket_resource_type = enif_init_resource_type(&env, "websocket", &websocket_resource_type_init, ERL_NIF_RT_CREATE, NULL); if (IS_NULL_PTR(platform->websocket_resource_type)) { fprintf(stderr, "Cannot initialize websocket_resource_type"); AVM_ABORT(); } + platform->tracked_object_resource_type = enif_init_resource_type(&env, "tracked_object", &tracked_object_resource_type_init, ERL_NIF_RT_CREATE, NULL); + if (IS_NULL_PTR(platform->tracked_object_resource_type)) { + fprintf(stderr, "Cannot initialize tracked_object resource type"); + AVM_ABORT(); + } + #ifndef AVM_NO_SMP platform->entropy_mutex = smp_mutex_create(); if (IS_NULL_PTR(platform->entropy_mutex)) { diff --git a/src/platforms/emscripten/src/main.c b/src/platforms/emscripten/src/main.c index 27e02c3a6..6f6c49650 100644 --- a/src/platforms/emscripten/src/main.c +++ b/src/platforms/emscripten/src/main.c @@ -31,11 +31,10 @@ #include #include +#include "lib/emscripten_sys.h" #include #include -#include "emscripten_sys.h" - static GlobalContext *global = NULL; static Module *main_module = NULL; @@ -138,6 +137,16 @@ em_promise_t call(const char *name, const char *message) return sys_enqueue_emscripten_call_message(global, name, message); } +/** + * @brief Gets a number representing TrackedObject identity. + * @return a TrackedObject id. + */ +EMSCRIPTEN_KEEPALIVE +size_t next_tracked_object_key() +{ + return sys_get_next_tracked_object_key(global); +} + /** * @brief Emscripten entry point * @details For node builds, this function is run in the main thread. For web