├── .github └── workflows │ ├── check.yml │ └── daily-tests.yml ├── .gitignore ├── LICENSE ├── README.md ├── assets └── simulatedExtismSdk.js ├── build.sh ├── cmd ├── bump-version │ └── main.go └── run-plugin │ └── main.go ├── examples ├── add │ ├── README.md │ ├── add.mbt │ ├── add.mbti │ └── moon.pkg.json ├── arrays │ ├── README.md │ ├── all-three.mbt │ ├── arrays.mbti │ ├── floats.mbt │ ├── index.html │ ├── ints.mbt │ ├── moon.pkg.json │ ├── plugin-functions.mbt │ └── strings.mbt ├── count-vowels │ ├── README.md │ ├── count-vowels-demo.png │ ├── count-vowels.mbt │ ├── count-vowels.mbti │ ├── index.html │ └── moon.pkg.json ├── greet │ ├── README.md │ ├── greet-demo.png │ ├── greet.mbt │ ├── greet.mbti │ ├── index.html │ └── moon.pkg.json ├── http-get │ ├── README.md │ ├── http-get.mbt │ ├── http-get.mbti │ └── moon.pkg.json └── kitchen-sink │ ├── README.md │ ├── kitchen-sink.mbt │ ├── kitchen-sink.mbti │ └── moon.pkg.json ├── favicon.ico ├── go.mod ├── go.sum ├── moon.mod.json ├── pdk ├── config │ ├── config.mbt │ ├── config.mbti │ └── moon.pkg.json ├── extism │ ├── env.mbt │ ├── extism.mbti │ └── moon.pkg.json ├── host │ ├── host.mbt │ ├── host.mbti │ ├── memory.mbt │ └── moon.pkg.json ├── http │ ├── header.mbt │ ├── http.mbt │ ├── http.mbti │ ├── method.mbt │ ├── method_test.mbt │ └── moon.pkg.json ├── moon.pkg.json ├── pdk.mbti ├── string.mbt ├── string_test.mbt └── var │ ├── moon.pkg.json │ ├── var.mbt │ └── var.mbti ├── run.sh ├── scripts ├── add.sh ├── arrays-floats.sh ├── arrays-ints.sh ├── arrays-object.sh ├── arrays-strings.sh ├── bump-version.sh ├── count-vowels.sh ├── debug-count-vowels.sh ├── go-run-count-vowels.sh ├── greet.sh ├── http-get.sh ├── kitchen-sink.sh └── python-server.sh └── update.sh /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: check 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | matrix: 13 | # os: [ubuntu-latest, macos-latest, windows-latest] 14 | os: [ubuntu-latest] 15 | 16 | runs-on: ${{ matrix.os }} 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - name: install-ubuntu 21 | if: ${{ matrix.os == 'ubuntu-latest' }} 22 | run: | 23 | /bin/bash -c "$(curl -fsSL https://cli.moonbitlang.com/install/unix.sh)" 24 | echo "/home/runner/.moon/bin" >> $GITHUB_PATH 25 | # - name: install-macos 26 | # if: ${{ matrix.os == 'macos-latest' }} 27 | # run: | 28 | # /bin/bash -c "$(curl -fsSL https://cli.moonbitlang.com/mac_intel_moon_setup.sh)" 29 | # echo "/Users/runner/.moon/bin" >> $GITHUB_PATH 30 | # - name: install-windows 31 | # if: ${{ matrix.os == 'windows-latest' }} 32 | # run: | 33 | # Set-ExecutionPolicy RemoteSigned -Scope CurrentUser; irm https://cli.moonbitlang.cn/windows_moon_setup.ps1 | iex 34 | # "C:\Users\runneradmin\.moon\bin" | Out-File -FilePath $env:GITHUB_PATH -Append 35 | 36 | - name: moon version 37 | run: | 38 | moon version --all 39 | moonrun --version 40 | 41 | - name: moon update 42 | run: | 43 | moon update 44 | 45 | - name: moon install 46 | run: | 47 | moon install 48 | 49 | - name: moon check 50 | run: moon check --deny-warn 51 | 52 | - name: moon info 53 | run: | 54 | moon info 55 | git diff --exit-code 56 | 57 | - name: moon test 58 | run: | 59 | moon test 60 | moon test --target js 61 | 62 | - name: format diff 63 | run: | 64 | moon fmt 65 | git diff --exit-code 66 | -------------------------------------------------------------------------------- /.github/workflows/daily-tests.yml: -------------------------------------------------------------------------------- 1 | name: Daily Tests 2 | on: 3 | schedule: 4 | - cron: "0 7 * * *" 5 | # Run around midnight Pacific time. 6 | # GitHub runs crons on UTC time. 7 | 8 | jobs: 9 | build: 10 | strategy: 11 | matrix: 12 | # os: [ubuntu-latest, macos-latest, windows-latest] 13 | os: [ubuntu-latest] 14 | 15 | runs-on: ${{ matrix.os }} 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: install-ubuntu 20 | if: ${{ matrix.os == 'ubuntu-latest' }} 21 | run: | 22 | /bin/bash -c "$(curl -fsSL https://cli.moonbitlang.com/install/unix.sh)" 23 | echo "/home/runner/.moon/bin" >> $GITHUB_PATH 24 | # - name: install-macos 25 | # if: ${{ matrix.os == 'macos-latest' }} 26 | # run: | 27 | # /bin/bash -c "$(curl -fsSL https://cli.moonbitlang.com/mac_intel_moon_setup.sh)" 28 | # echo "/Users/runner/.moon/bin" >> $GITHUB_PATH 29 | # - name: install-windows 30 | # if: ${{ matrix.os == 'windows-latest' }} 31 | # run: | 32 | # Set-ExecutionPolicy RemoteSigned -Scope CurrentUser; irm https://cli.moonbitlang.cn/windows_moon_setup.ps1 | iex 33 | # "C:\Users\runneradmin\.moon\bin" | Out-File -FilePath $env:GITHUB_PATH -Append 34 | 35 | - name: moon version 36 | run: | 37 | moon version --all 38 | moonrun --version 39 | 40 | - name: moon update 41 | run: | 42 | moon update 43 | 44 | - name: moon install 45 | run: | 46 | moon install 47 | 48 | - name: moon check 49 | # run: moon check --deny-warn 50 | # Allow warnings during development: 51 | run: moon check 52 | 53 | - name: moon info 54 | run: | 55 | moon info 56 | git diff --exit-code 57 | 58 | - name: moon test 59 | run: | 60 | moon test 61 | moon test --target js 62 | 63 | - name: format diff 64 | run: | 65 | moon fmt 66 | git diff --exit-code 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .mooncakes/ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # extism/moonbit-pdk 2 | [![check](https://github.com/extism/moonbit-pdk/actions/workflows/check.yml/badge.svg)](https://github.com/extism/moonbit-pdk/actions/workflows/check.yml) 3 | 4 | This is an [Extism PDK] that can be used to write [Extism Plug-ins] using the [MoonBit] programming language. 5 | 6 | [Extism PDK]: https://extism.org/docs/concepts/pdk 7 | [Extism Plug-ins]: https://extism.org/docs/concepts/plug-in 8 | [MoonBit]: https://www.moonbitlang.com/ 9 | 10 | ## Install 11 | 12 | Add the library to your project as a dependency with the `moon` tool: 13 | 14 | ```bash 15 | moon add extism/moonbit-pdk 16 | ``` 17 | 18 | ## Reference Documentation 19 | 20 | You can find the reference documentation for this library on [mooncakes.io]: 21 | 22 | * [extism/moonbit-pdk overview and status] 23 | * [extism/moonbit-pdk/pdk/config] 24 | * [extism/moonbit-pdk/pdk/host] 25 | * [extism/moonbit-pdk/pdk/http] 26 | * [extism/moonbit-pdk/pdk/var] 27 | 28 | [mooncakes.io]: https://mooncakes.io 29 | [extism/moonbit-pdk overview and status]: https://mooncakes.io/docs/#/extism/moonbit-pdk/ 30 | [extism/moonbit-pdk/pdk/config]: https://mooncakes.io/docs/#/extism/moonbit-pdk/pdk/config/members 31 | [extism/moonbit-pdk/pdk/host]: https://mooncakes.io/docs/#/extism/moonbit-pdk/pdk/host/members 32 | [extism/moonbit-pdk/pdk/http]: https://mooncakes.io/docs/#/extism/moonbit-pdk/pdk/http/members 33 | [extism/moonbit-pdk/pdk/var]: https://mooncakes.io/docs/#/extism/moonbit-pdk/pdk/var/members 34 | 35 | Examples can also be found there: 36 | 37 | * [extism/moonbit-pdk/examples/add] 38 | * [extism/moonbit-pdk/examples/arrays] 39 | * [extism/moonbit-pdk/examples/count-vowels] 40 | * [extism/moonbit-pdk/examples/greet] 41 | * [extism/moonbit-pdk/examples/http-get] 42 | * [extism/moonbit-pdk/examples/kitchen-sink] 43 | 44 | [extism/moonbit-pdk/examples/add]: https://mooncakes.io/docs/#/extism/moonbit-pdk/examples/add/members 45 | [extism/moonbit-pdk/examples/arrays]: https://mooncakes.io/docs/#/extism/moonbit-pdk/examples/arrays/members 46 | [extism/moonbit-pdk/examples/count-vowels]: https://mooncakes.io/docs/#/extism/moonbit-pdk/examples/count-vowels/members 47 | [extism/moonbit-pdk/examples/greet]: https://mooncakes.io/docs/#/extism/moonbit-pdk/examples/greet/members 48 | [extism/moonbit-pdk/examples/http-get]: https://mooncakes.io/docs/#/extism/moonbit-pdk/examples/http-get/members 49 | [extism/moonbit-pdk/examples/kitchen-sink]: https://mooncakes.io/docs/#/extism/moonbit-pdk/examples/kitchen-sink/members 50 | 51 | ## Getting Started 52 | 53 | The goal of writing an [Extism plug-in](https://extism.org/docs/concepts/plug-in) 54 | is to compile your MoonBit code to a Wasm module with exported functions that the 55 | host application can invoke. The first thing you should understand is creating an export. 56 | Let's write a simple program that exports a `greet` function which will take 57 | a name as a string and return a greeting string. 58 | 59 | First, install the `moon` CLI tool: 60 | 61 | See https://www.moonbitlang.com/download/ for instructions for your platform. 62 | 63 | Create a new MoonBit project directory using the `moon` tool and initialize 64 | the project: 65 | 66 | ```bash 67 | moon new greet 68 | cd greet 69 | ``` 70 | 71 | Next, add this Extism PDK to the project and remove the default "lib" example: 72 | 73 | ```bash 74 | moon add extism/moonbit-pdk 75 | rm -rf lib 76 | ``` 77 | 78 | Now paste this into your `main/main.mbt` file: 79 | 80 | ```rust 81 | pub fn greet() -> Int { 82 | let name = @host.input_string() 83 | let greeting = "Hello, \{name}!" 84 | @host.output_string(greeting) 85 | 0 // success 86 | } 87 | 88 | fn main { 89 | 90 | } 91 | ``` 92 | 93 | Then paste this into your `main/moon.pkg.json` file to export the `greet` function 94 | and include the `@host` import into your plugin: 95 | 96 | ```json 97 | { 98 | "import": [ 99 | "extism/moonbit-pdk/pdk/host" 100 | ], 101 | "link": { 102 | "wasm": { 103 | "exports": [ 104 | "greet" 105 | ], 106 | "export-memory-name": "memory" 107 | } 108 | } 109 | } 110 | ``` 111 | 112 | Some things to note about this code: 113 | 114 | 1. The `moon.pkg.json` file is required. This marks the greet function as an export with the name `greet` that can be called by the host. 115 | 2. We need a `main` but it is unused. 116 | 3. Exports in the MoonBit PDK are coded to the raw ABI. You get parameters from the host by calling [`@host.input*` functions](https://mooncakes.io/docs/#/extism/moonbit-pdk/pdk/host/members?id=input) and you send return values back with the [`@host.output*` functions](https://mooncakes.io/docs/#/extism/moonbit-pdk/pdk/host/members?id=output). 117 | 4. An Extism export expects an i32 (a MoonBit `Int`) return code. `0` is success and `1` (or any other value) is a failure. 118 | 119 | Finally, compile this with the command: 120 | 121 | ```bash 122 | moon build --target wasm 123 | ``` 124 | 125 | We can now test `plugin.wasm` using the [Extism CLI](https://github.com/extism/cli)'s `run` 126 | command: 127 | 128 | ```bash 129 | extism call target/wasm/release/build/main/main.wasm greet --input "Benjamin" --wasi 130 | # => Hello, Benjamin! 131 | ``` 132 | 133 | > **Note**: We also have a web-based, plug-in tester called the [Extism Playground](https://playground.extism.org/) 134 | 135 | ### More Exports: Error Handling 136 | 137 | Suppose we want to re-write our greeting module to never greet Benjamins. 138 | We can use [`@host.set_error`](https://mooncakes.io/docs/#/extism/moonbit-pdk/pdk/host/members?id=set_error): 139 | 140 | ```rust 141 | pub fn greet() -> Int { 142 | let name = @host.input_string() 143 | if name == "Benjamin" { 144 | @host.set_error("Sorry, we don't greet Benjamins!") 145 | return 1 // failure 146 | } 147 | let greeting = "Hello, \{name}!" 148 | @host.output_string(greeting) 149 | 0 // success 150 | } 151 | ``` 152 | 153 | Now when we try again: 154 | 155 | ```bash 156 | moon build --target wasm 157 | extism call target/wasm/release/build/main/main.wasm greet --input "Benjamin" --wasi 158 | # => Error: Sorry, we don't greet Benjamins! 159 | echo $? # print last status code 160 | # => 1 161 | extism call target/wasm/release/build/main/main.wasm greet --input "Zach" --wasi 162 | # => Hello, Zach! 163 | echo $? 164 | # => 0 165 | ``` 166 | 167 | ### JSON 168 | 169 | Extism export functions simply take bytes in and bytes out. Those can be whatever you want them to be. 170 | A common way to get more complex types to and from the host is with JSON: 171 | (MoonBit currently requires a bit of boilerplate to handle JSON I/O but 172 | hopefully this situation will improve as the standard library is fleshed out.) 173 | 174 | ```rust 175 | struct Add { 176 | a : Int 177 | b : Int 178 | } 179 | 180 | pub fn Add::from_json(value : Json) -> Add? { 181 | // From: https://github.com/moonbitlang/core/issues/892#issuecomment-2306068783 182 | match value { 183 | { "a": Number(a), "b": Number(b) } => Some({ a: a.to_int(), b: b.to_int() }) 184 | _ => None 185 | } 186 | } 187 | 188 | type! ParseError String derive(Show) 189 | 190 | pub fn Add::parse(s : String) -> Add!ParseError { 191 | match @json.parse?(s) { 192 | Ok(jv) => 193 | match Add::from_json(jv) { 194 | Some(value) => value 195 | None => raise ParseError("unable to parse Add \{s}") 196 | } 197 | Err(e) => raise ParseError("unable to parse Add \{s}: \{e}") 198 | } 199 | } 200 | 201 | struct Sum { 202 | sum : Int 203 | } derive(ToJson) 204 | 205 | pub fn add() -> Int { 206 | let input = @host.input_string() 207 | let params = try { 208 | Add::parse!(input) 209 | } catch { 210 | ParseError(e) => { 211 | @host.set_error(e) 212 | return 1 213 | } 214 | } 215 | // 216 | let sum = { sum: params.a + params.b } 217 | let json_value = sum.to_json() 218 | @host.output_json_value(json_value) 219 | 0 // success 220 | } 221 | ``` 222 | 223 | 224 | Export your `add` function in `main/moon.pkg.json`: 225 | 226 | ```json 227 | { 228 | "import": [ 229 | "extism/moonbit-pdk/pdk/host" 230 | ], 231 | "link": { 232 | "wasm": { 233 | "exports": [ 234 | "add" 235 | ], 236 | "export-memory-name": "memory" 237 | } 238 | } 239 | } 240 | ``` 241 | 242 | Then compile and run: 243 | 244 | ```bash 245 | moon build --target wasm 246 | extism call plugin.wasm add --input='{"a": 20, "b": 21}' --wasi 247 | # => {"sum":41} 248 | ``` 249 | 250 | ## Configs 251 | 252 | Configs are key-value pairs that can be passed in by the host when creating a plug-in. 253 | These can be useful to statically configure the plug-in with some data that exists 254 | across every function call. 255 | 256 | Here is a trivial example using [`config.get`](https://mooncakes.io/docs/#/extism/moonbit-pdk/pdk/config/members?id=get): 257 | 258 | ```rust 259 | pub fn greet() -> Int { 260 | let user = match @config.get("user") { 261 | Some(user) => user 262 | None => { 263 | @host.set_error("This plug-in requires a 'user' key in the config") 264 | return 1 // failure 265 | } 266 | } 267 | let greeting = "Hello, \{user}!" 268 | @host.output_string(greeting) 269 | 0 // success 270 | } 271 | ``` 272 | 273 | Remember to import the `config` and `host` packages in `main/moon.pkg.json` and 274 | export your function: 275 | 276 | ```json 277 | { 278 | "import": [ 279 | "extism/moonbit-pdk/pdk/config", 280 | "extism/moonbit-pdk/pdk/host" 281 | ], 282 | "link": { 283 | "wasm": { 284 | "exports": [ 285 | "greet" 286 | ], 287 | "export-memory-name": "memory" 288 | } 289 | } 290 | } 291 | ``` 292 | 293 | To test it, the [Extism CLI](https://github.com/extism/cli) has a `--config` option that lets you pass in `key=value` pairs: 294 | 295 | ```bash 296 | moon build --target wasm 297 | extism call target/wasm/release/build/main/main.wasm greet --config user=Benjamin 298 | # => Hello, Benjamin! 299 | extism call target/wasm/release/build/main/main.wasm greet 300 | # => Error: This plug-in requires a 'user' key in the config 301 | ``` 302 | 303 | ## Variables 304 | 305 | Variables are another key-value mechanism but are a mutable data store that 306 | will persist across function calls. These variables will persist as long as the 307 | host has loaded and not freed the plug-in. 308 | 309 | ```rust 310 | pub fn count() -> Int { 311 | let mut count = match @var.get_int("count") { 312 | Some(v) => v 313 | None => 0 314 | } 315 | count = count + 1 316 | @var.set_int("count", count) 317 | let s = count.to_string() 318 | @host.output_string(s) 319 | 0 // success 320 | } 321 | ``` 322 | 323 | > **Note**: Use the untyped variant [`@var.set_bytes`](https://mooncakes.io/docs/#/extism/moonbit-pdk/pdk/var/members?id=set_bytes) 324 | > to handle your own types. 325 | 326 | Remember to import the `host` and `var` packages in `main/moon.pkg.json` and 327 | export your function: 328 | 329 | ```json 330 | { 331 | "import": [ 332 | "extism/moonbit-pdk/pdk/host", 333 | "extism/moonbit-pdk/pdk/var" 334 | ], 335 | "link": { 336 | "wasm": { 337 | "exports": [ 338 | "count" 339 | ], 340 | "export-memory-name": "memory" 341 | } 342 | } 343 | } 344 | ``` 345 | 346 | ## Logging 347 | 348 | Because Wasm modules by default do not have access to the system, printing to 349 | stdout won't work (unless you use WASI). Extism provides simple 350 | [logging functions](https://mooncakes.io/docs/#/extism/moonbit-pdk/pdk/host/members?id=log_debug_str) 351 | that allow you to use the host application to log without having to give the 352 | plug-in permission to make syscalls. 353 | 354 | ```rust 355 | pub fn log_stuff() -> Int { 356 | @host.log_info_str("An info log!") 357 | @host.log_debug_str("A debug log!") 358 | @host.log_warn_str("A warn log!") 359 | @host.log_error_str("An error log!") 360 | 0 // success 361 | } 362 | ``` 363 | 364 | From [Extism CLI](https://github.com/extism/cli): 365 | 366 | ```bash 367 | moon build --target wasm 368 | extism call target/wasm/release/build/main/main.wasm log_stuff --wasi --log-level=trace 369 | # => 2024/07/09 11:37:30 No runtime detected 370 | # => 2024/07/09 11:37:30 Calling function : log_stuff 371 | # => 2024/07/09 11:37:30 An info log! 372 | # => 2024/07/09 11:37:30 A debug log! 373 | # => 2024/07/09 11:37:30 A warn log! 374 | # => 2024/07/09 11:37:30 An error log! 375 | ``` 376 | 377 | > *Note*: From the CLI you need to pass a level with `--log-level`. 378 | > If you are running the plug-in in your own host using one of our SDKs, you need 379 | > to make sure that you call `set_log_file` to `"stdout"` or some file location. 380 | 381 | ## HTTP 382 | 383 | Sometimes it is useful to let a plug-in [make HTTP calls](https://mooncakes.io/docs/#/extism/moonbit-pdk/pdk/http/members?id=send). 384 | [See this example](examples/http-get/http-get.mbt). 385 | 386 | ```rust 387 | pub fn http_get() -> Int { 388 | // create an HTTP Request (without relying on WASI), set headers as needed 389 | let req = @http.new_request( 390 | @http.Method::GET, 391 | "https://jsonplaceholder.typicode.com/todos/1", 392 | ) 393 | req.header.set("some-name", "some-value") 394 | req.header.set("another", "again") 395 | // send the request, get response back 396 | let res = req.send() 397 | 398 | // zero-copy send output to host 399 | res.output() 400 | 0 // success 401 | } 402 | ``` 403 | 404 | By default, Extism modules cannot make HTTP requests unless you specify which 405 | hosts it can connect to. You can use `--alow-host` in the Extism CLI to set this: 406 | 407 | ```bash 408 | extism call \ 409 | target/wasm/release/build/examples/http-get/http-get.wasm \ 410 | http_get \ 411 | --wasi \ 412 | --allow-host='*.typicode.com' 413 | # => { 414 | # => "userId": 1, 415 | # => "id": 1, 416 | # => "title": "delectus aut autem", 417 | # => "completed": false 418 | # => } 419 | ``` 420 | 421 | ## Imports (Host Functions) 422 | 423 | Like any other code module, Wasm not only lets you export functions to the outside world, you can 424 | import them too. Host Functions allow a plug-in to import functions defined in the host. For example, 425 | if your host application is written in Python, it can pass a Python function down to your MoonBit plug-in 426 | where you can invoke it. 427 | 428 | This topic can get fairly complicated and we have not yet fully abstracted the Wasm knowledge you need 429 | to do this correctly. So we recommend reading our [concept doc on Host Functions](https://extism.org/docs/concepts/host-functions) 430 | before you get started. 431 | 432 | ### A Simple Example 433 | 434 | Host functions have a similar interface as exports. You just need to declare them 435 | as external in your `main.mbt`. You only declare the interface as it is the host's 436 | responsibility to provide the implementation: 437 | 438 | ```rust 439 | pub fn a_python_func(offset : Int64) -> Int64 = "extism:host/user" "a_python_func" 440 | ``` 441 | 442 | We should be able to call this function as a normal Go function. Note that we need to manually handle the pointer casting: 443 | 444 | ```rust 445 | pub fn hello_from_python() -> Int { 446 | let msg = "An argument to send to Python" 447 | let mem = @host.allocate_string(msg) 448 | let ptr = a_python_func(mem.offset) 449 | mem.free() 450 | let rmem = @host.find_memory(ptr) 451 | let response = rmem.to_string() 452 | @host.output_string(response) 453 | return 0 454 | } 455 | ``` 456 | 457 | ### Testing it out 458 | 459 | We can't really test this from the Extism CLI as something must provide the implementation. So let's 460 | write out the Python side here. Check out the [docs for Host SDKs](https://extism.org/docs/concepts/host-sdk) 461 | to implement a host function in a language of your choice. 462 | 463 | ```python 464 | from extism import host_fn, Plugin 465 | 466 | @host_fn() 467 | def a_python_func(input: str) -> str: 468 | # just printing this out to prove we're in Python land 469 | print("Hello from Python!") 470 | 471 | # let's just add "!" to the input string 472 | # but you could imagine here we could add some 473 | # applicaiton code like query or manipulate the database 474 | # or our application APIs 475 | return input + "!" 476 | ``` 477 | 478 | Now when we load the plug-in we pass the host function: 479 | 480 | ```python 481 | manifest = {"wasm": [{"path": "target/wasm/release/build/main/main.wasm"}]} 482 | plugin = Plugin(manifest, functions=[a_python_func], wasi=True) 483 | result = plugin.call('hello_from_python', b'').decode('utf-8') 484 | print(result) 485 | ``` 486 | 487 | ```bash 488 | moon build --target wasm 489 | python3 -m pip install extism 490 | python3 app.py 491 | # => Hello from Python! 492 | # => An argument to send to Python! 493 | ``` 494 | 495 | > **Note**: This fails on my Mac M2 Max with some weird system error 496 | > but works great on my Linux Mint Cinnamon box. 497 | 498 | ## For PDK Devs: Building the PDK locally 499 | 500 | Before building, you must have already installed the MoonBit programming language, 501 | the [Go] programming language, and the [Extism CLI tool]. 502 | 503 | To install MoonBit, follow the instructions here (it is super-easy with VSCode): 504 | https://www.moonbitlang.com/download/ 505 | 506 | Then, to build this PDK, clone the repo, and type: 507 | 508 | ```bash 509 | moon update && moon install 510 | ./build.sh 511 | ``` 512 | 513 | [Extism CLI tool]: https://extism.org/docs/install/ 514 | [Go]: https://go.dev/ 515 | 516 | ### Run 517 | 518 | To run the examples, type: 519 | 520 | ```bash 521 | ./run.sh 522 | ``` 523 | 524 | ## Status 525 | 526 | The code has been updated to support compiler: 527 | 528 | ```bash 529 | $ moon version --all 530 | moon 0.1.20250529 (daaecb6 2025-05-29) ~/.moon/bin/moon 531 | moonc v0.1.20250529+8a98c8e02 ~/.moon/bin/moonc 532 | moonrun 0.1.20250529 (daaecb6 2025-05-29) ~/.moon/bin/moonrun 533 | ``` 534 | 535 | ## Reach Out! 536 | 537 | Have a question or just want to drop in and say hi? [Hop on the Discord](https://extism.org/discord)! 538 | -------------------------------------------------------------------------------- /assets/simulatedExtismSdk.js: -------------------------------------------------------------------------------- 1 | // This is a simulated Extism SDK written in JavaScript in order to assist 2 | // in the debugging of the MoonBit Extism PDK. 3 | 4 | // Adapted from: https://dmitripavlutin.com/timeout-fetch-request/ 5 | export const fetchWithTimeout = async (resource, options = {}) => { 6 | const { timeout = 8000 } = options // 8000 ms = 8 seconds 7 | 8 | const controller = new AbortController() 9 | const id = setTimeout(() => controller.abort(), timeout) 10 | const response = await fetch(resource, { 11 | ...options, 12 | signal: controller.signal, 13 | }) 14 | clearTimeout(id) 15 | return response 16 | } 17 | 18 | // `log` and `flust` are useful for debugging the wasm-gc or wasm targets with `println()`: 19 | export const [log, flush] = (() => { 20 | var buffer = [] 21 | function flush() { 22 | if (buffer.length > 0) { 23 | console.log(new TextDecoder("utf-16").decode(new Uint16Array(buffer).valueOf())) 24 | buffer = [] 25 | } 26 | } 27 | function log(ch) { 28 | if (ch == '\n'.charCodeAt(0)) { flush() } 29 | else if (ch == '\r'.charCodeAt(0)) { /* noop */ } 30 | else { buffer.push(ch) } 31 | } 32 | return [log, flush] 33 | })() 34 | 35 | const memory = new WebAssembly.Memory({ initial: 1, maximum: 1, shared: false }) 36 | const fakeAlloc = { offset: 0, buffers: {} } 37 | const alloc = (lengthBigInt) => { 38 | const offset = fakeAlloc.offset 39 | const length = Number(lengthBigInt) 40 | fakeAlloc.buffers[offset] = { 41 | offset, 42 | length, 43 | buffer: new Uint8Array(memory.buffer, offset, length), 44 | } 45 | fakeAlloc.offset += length 46 | return BigInt(offset) 47 | } 48 | const allocAndCopy = (str) => { 49 | const offsetBigInt = alloc(BigInt(str.length)) 50 | const offset = Number(offsetBigInt) 51 | const b = fakeAlloc.buffers[offset] 52 | for (let i = 0; i < str.length; i++) { b.buffer[i] = str.charCodeAt(i) } 53 | return offsetBigInt 54 | } 55 | const decodeOffset = (offset) => new TextDecoder().decode(fakeAlloc.buffers[offset].buffer) 56 | const lastHttpResponse = { statusCode: 0 } 57 | const http_request = async (reqOffsetBigInt, bodyOffsetBigInt) => { 58 | const req = JSON.parse(decodeOffset(reqOffsetBigInt)) 59 | const body = bodyOffsetBigInt ? decodeOffset(bodyOffsetBigInt) : '' 60 | console.log(`http_request: req=${JSON.stringify(req)}`) 61 | console.log(`http_request: body=${body}`) 62 | const fetchParams = { 63 | method: req.method, 64 | headers: req.header, 65 | } 66 | if (body) { fetchParams.body = body } 67 | const response = await fetchWithTimeout(req.url, fetchParams) 68 | const result = await response.text() 69 | console.log(`result=${result}`) 70 | lastHttpResponse.statusCode = response.status 71 | return allocAndCopy(result) 72 | } 73 | const http_status_code = () => lastHttpResponse.statusCode 74 | 75 | export const configs = {} // no configs to start with 76 | export const vars = {} // no vars to start with 77 | 78 | export const inputString = { value: '' } // allows for exporting 79 | 80 | export const importObject = { 81 | "extism:host/env": { 82 | alloc, 83 | config_get: (offsetBigInt) => { 84 | const offset = Number(offsetBigInt) 85 | const key = decodeOffset(offset) 86 | // console.log(`config_get(${offset}) = configs[${key}] = ${configs[key]}`) 87 | if (!configs[key]) { return BigInt(0) } 88 | return allocAndCopy(configs[key]) 89 | }, 90 | free: () => { }, // noop for now. 91 | http_request, 92 | http_status_code, 93 | input_length: () => BigInt(inputString.value.length), 94 | input_load_u8: (offsetBigInt) => { 95 | const offset = Number(offsetBigInt) 96 | if (offset < inputString.value.length) { return inputString.value.charCodeAt(offset) } 97 | console.error(`input_load_u8: wasm requested offset(${offset}) > inputString.value.length(${inputString.value.length})`) 98 | return 0 99 | }, 100 | length: (offsetBigInt) => { 101 | const offset = Number(offsetBigInt) 102 | const b = fakeAlloc.buffers[offset] 103 | if (!b) { return BigInt(0) } 104 | // console.log(`length(${offset}) = ${b.length}`) 105 | return BigInt(b.length) 106 | }, 107 | load_u8: (offsetBigInt) => { 108 | const offset = Number(offsetBigInt) 109 | const bs = Object.keys(fakeAlloc.buffers).filter((key) => { 110 | const b = fakeAlloc.buffers[key] 111 | return (offset >= b.offset && offset < b.offset + b.length) 112 | }) 113 | if (bs.length !== 1) { 114 | console.error(`load_u8: offset ${offset} not found`) 115 | return 0 116 | } 117 | const key = bs[0] 118 | const b = fakeAlloc.buffers[key] 119 | const byte = b.buffer[offset - key] 120 | // console.log(`load_u8(${offset}) = ${byte}`) 121 | return byte 122 | }, 123 | log_info: (offset) => console.info(`log_info: ${decodeOffset(offset)}`), 124 | log_debug: (offset) => console.log(`log_debug: ${decodeOffset(offset)}`), 125 | log_error: (offset) => console.error(`log_error: ${decodeOffset(offset)}`), 126 | log_warn: (offset) => console.warn(`log_warn: ${decodeOffset(offset)}`), 127 | output_set: (offset) => console.log(decodeOffset(offset)), 128 | error_set: (offset) => console.error(decodeOffset(offset)), 129 | store_u8: (offsetBigInt, byte) => { 130 | const offset = Number(offsetBigInt) 131 | Object.keys(fakeAlloc.buffers).forEach((key) => { 132 | const b = fakeAlloc.buffers[key] 133 | if (offset >= b.offset && offset < b.offset + b.length) { 134 | b.buffer[offset - key] = byte 135 | // console.log(`store_u8(${offset})=${byte}`) 136 | // if (offset == b.offset + b.length - 1) { 137 | // console.log(`store_u8 completed offset=${key}..${offset}, length=${b.length}: '${decodeOffset(key)}'`) 138 | // } 139 | } 140 | }) 141 | }, 142 | var_get: (offsetBigInt) => { 143 | const offset = Number(offsetBigInt) 144 | const key = decodeOffset(offset) 145 | // console.log(`var_get(${offset}) = vars[${key}] = ${vars[key]}`) 146 | if (!vars[key]) { return BigInt(0) } 147 | return vars[key] // BigInt 148 | }, 149 | var_set: (offsetBigInt, bufOffsetBigInt) => { 150 | const offset = Number(offsetBigInt) 151 | const key = decodeOffset(offset) 152 | // console.log(`var_set(${offset}, ${bufOffsetBigInt}) = vars[${key}]`) 153 | vars[key] = bufOffsetBigInt 154 | }, 155 | }, 156 | spectest: { print_char: log }, 157 | } 158 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -ex 2 | 3 | # wasm-gc can be useful for debugging in the browser: 4 | # moon build --target wasm-gc --output-wat 5 | # moon build --target wasm-gc 6 | # moon build --target wasm --output-wat 7 | 8 | moon fmt 9 | moon build --target wasm 10 | moon test 11 | -------------------------------------------------------------------------------- /cmd/bump-version/main.go: -------------------------------------------------------------------------------- 1 | // bump-version reads the `moon.mod.json` file, parses the "version" line, 2 | // bumps the minor version by one, and writes back the file. 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "log" 8 | "os" 9 | "regexp" 10 | "strconv" 11 | "strings" 12 | ) 13 | 14 | const filename = "moon.mod.json" 15 | 16 | var ( 17 | versionRE = regexp.MustCompile(`"version": "\d+\.(\d+)\.\d+"`) 18 | ) 19 | 20 | func main() { 21 | buf, err := os.ReadFile(filename) 22 | must(err) 23 | 24 | m := versionRE.FindStringSubmatch(string(buf)) 25 | if len(m) != 2 { 26 | log.Fatalf("unable to find version in %v", filename) 27 | } 28 | 29 | minor, err := strconv.Atoi(m[1]) 30 | must(err) 31 | 32 | oldStr, newStr := fmt.Sprintf(".%v.", m[1]), fmt.Sprintf(".%v.", minor+1) 33 | newVersion := strings.Replace(m[0], oldStr, newStr, 1) 34 | outStr := strings.Replace(string(buf), m[0], newVersion, 1) 35 | 36 | must(os.WriteFile(filename, []byte(outStr), 0644)) 37 | 38 | log.Printf("Done.") 39 | } 40 | 41 | func must(err error) { 42 | if err != nil { 43 | log.Fatal(err) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /cmd/run-plugin/main.go: -------------------------------------------------------------------------------- 1 | // -*- compile-command: "go run main.go -wasm ../../target/wasm/release/build/examples/count-vowels/count-vowels.wasm"; -*- 2 | 3 | // run-plugin is a simple Go script that calls a Extism plugin. 4 | // In theory, it could be used to start up the delv debugger (https://github.com/go-delve/delve) 5 | // and allow you to step through the wasm code, although there must be a better way. 6 | package main 7 | 8 | import ( 9 | "context" 10 | "flag" 11 | "fmt" 12 | "log" 13 | "os" 14 | "path/filepath" 15 | 16 | extism "github.com/extism/go-sdk" 17 | ) 18 | 19 | var ( 20 | input = flag.String("input", "Hello, World!", "String input to Extism function") 21 | funcName = flag.String("func", "count_vowels", "Extism function name to call") 22 | wasmFile = flag.String("wasm", "count-vowels.wasm", "Extism plugin to debug") 23 | ) 24 | 25 | func main() { 26 | flag.Parse() 27 | 28 | manifest := extism.Manifest{ 29 | Wasm: []extism.Wasm{ 30 | extism.WasmFile{ 31 | Path: *wasmFile, 32 | }, 33 | }, 34 | } 35 | 36 | ctx := context.Background() 37 | config := extism.PluginConfig{ 38 | EnableWasi: true, 39 | // LogLevel: extism.LogLevelTrace, 40 | } 41 | plugin, err := extism.NewPlugin(ctx, manifest, config, []extism.HostFunction{}) 42 | if err != nil { 43 | wd, _ := os.Getwd() 44 | log.Printf("Current work dir: %v", wd) 45 | log.Printf("Attempted to load: %v", filepath.Join(wd, *wasmFile)) 46 | fmt.Printf("Failed to initialize plugin: %v\n", err) 47 | os.Exit(1) 48 | } 49 | 50 | exitCode, out, err := plugin.Call(*funcName, []byte(*input)) 51 | if err != nil { 52 | log.Fatalf("exit code=%v: %v\nplugin.Call output:\n%s", exitCode, err, out) 53 | } 54 | 55 | fmt.Printf("%s\n", out) 56 | } 57 | -------------------------------------------------------------------------------- /examples/add/README.md: -------------------------------------------------------------------------------- 1 | # examples/add 2 | 3 | The `add.wasm` plugin can be run from the top-level of the repo by 4 | typing: 5 | 6 | ```bash 7 | $ ./build.sh 8 | $ ./scripts/add.sh '{"a": 20, "b": 21}' 9 | # => {"sum":41} 10 | ``` 11 | -------------------------------------------------------------------------------- /examples/add/add.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | priv struct Add { 3 | a : Int 4 | b : Int 5 | } derive(FromJson) 6 | 7 | ///| 8 | priv struct Sum { 9 | sum : Int 10 | } derive(ToJson) 11 | 12 | ///| 13 | pub fn add() -> Int { 14 | let input = @host.input_string() 15 | let params : Add = try @json.from_json!(@json.parse!(input)) catch { 16 | e => { 17 | @host.set_error(e.to_string()) 18 | return 1 19 | } 20 | } 21 | // 22 | let sum = { sum: params.a + params.b } 23 | @host.output_json_value(sum.to_json()) 24 | 0 // success 25 | } 26 | 27 | ///| 28 | test "Sum::to_json works as expected" { 29 | let sum = { sum: 42 } 30 | let got = sum.to_json().stringify() 31 | let want = 32 | #|{"sum":42} 33 | assert_eq!(got, want) 34 | } 35 | -------------------------------------------------------------------------------- /examples/add/add.mbti: -------------------------------------------------------------------------------- 1 | package "extism/moonbit-pdk/examples/add" 2 | 3 | // Values 4 | fn add() -> Int 5 | 6 | // Types and methods 7 | 8 | // Type aliases 9 | 10 | // Traits 11 | 12 | -------------------------------------------------------------------------------- /examples/add/moon.pkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "import": [ 3 | "extism/moonbit-pdk/pdk/host" 4 | ], 5 | "link": { 6 | "wasm": { 7 | "exports": [ 8 | "add" 9 | ], 10 | "export-memory-name": "memory" 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /examples/arrays/README.md: -------------------------------------------------------------------------------- 1 | # examples/arrays 2 | 3 | The `arrays.wasm` plugin can be run from the top-level of the repo by 4 | typing: 5 | 6 | ```bash 7 | $ ./build.sh 8 | $ ./scripts/python-server.sh 9 | ``` 10 | 11 | Then open your browser window to: 12 | http://localhost:8080/examples/arrays 13 | -------------------------------------------------------------------------------- /examples/arrays/all-three.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | /// `AllThree` represents a JSON object with all three array types. 3 | pub(all) struct AllThree { 4 | ints : Array[Int] 5 | floats : Array[Double] 6 | strings : Array[String] 7 | } derive(Eq, Show, ToJson) 8 | 9 | ///| 10 | /// `process_all_three` processes all three array types. 11 | pub fn process_all_three(all3 : AllThree) -> AllThree { 12 | { 13 | ints: process_ints(all3.ints), 14 | floats: process_floats(all3.floats), 15 | strings: process_strings(all3.strings), 16 | } 17 | } 18 | 19 | ///| 20 | test "AllThree::to_json works as expected" { 21 | let all3 = { ints: [1, 2, 3], floats: [1, 2, 3], strings: ["1", "2", "3"] } 22 | let got = all3.to_json().stringify() 23 | let want = 24 | #|{"ints":[1,2,3],"floats":[1,2,3],"strings":["1","2","3"]} 25 | assert_eq!(got, want) 26 | } 27 | -------------------------------------------------------------------------------- /examples/arrays/arrays.mbti: -------------------------------------------------------------------------------- 1 | package "extism/moonbit-pdk/examples/arrays" 2 | 3 | // Values 4 | fn all_three_object() -> Int 5 | 6 | fn process_all_three(AllThree) -> AllThree 7 | 8 | fn process_floats(Array[Double]) -> Array[Double] 9 | 10 | fn process_ints(Array[Int]) -> Array[Int] 11 | 12 | fn process_strings(Array[String]) -> Array[String] 13 | 14 | fn progressive_concat_strings() -> Int 15 | 16 | fn progressive_sum_floats() -> Int 17 | 18 | fn progressive_sum_ints() -> Int 19 | 20 | // Types and methods 21 | pub(all) struct AllThree { 22 | ints : Array[Int] 23 | floats : Array[Double] 24 | strings : Array[String] 25 | } 26 | impl Eq for AllThree 27 | impl Show for AllThree 28 | impl ToJson for AllThree 29 | 30 | // Type aliases 31 | 32 | // Traits 33 | 34 | -------------------------------------------------------------------------------- /examples/arrays/floats.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | /// `process_floats` sums up an array of floats. 3 | pub fn process_floats(floats : Array[Double]) -> Array[Double] { 4 | let mut sum = 0.0 5 | floats.eachi(fn(index, value) { 6 | sum += value 7 | floats[index] = sum 8 | }) 9 | floats 10 | } 11 | -------------------------------------------------------------------------------- /examples/arrays/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /examples/arrays/ints.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | /// `process_ints` sums up an array of ints. 3 | pub fn process_ints(ints : Array[Int]) -> Array[Int] { 4 | let mut sum = 0 5 | ints.eachi(fn(index, value) { 6 | sum += value 7 | ints[index] = sum 8 | }) 9 | ints 10 | } 11 | -------------------------------------------------------------------------------- /examples/arrays/moon.pkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "import": [ 3 | "extism/moonbit-pdk/pdk/host" 4 | ], 5 | "link": { 6 | "wasm": { 7 | "exports": [ 8 | "all_three_object", 9 | "progressive_sum_ints", 10 | "progressive_sum_floats", 11 | "progressive_concat_strings" 12 | ], 13 | "export-memory-name": "memory" 14 | }, 15 | "wasm-gc": { 16 | "exports": [ 17 | "all_three_object", 18 | "progressive_sum_ints", 19 | "progressive_sum_floats", 20 | "progressive_concat_strings" 21 | ], 22 | "export-memory-name": "memory" 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /examples/arrays/plugin-functions.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | /// `progressive_sum_ints` tests passing arrays of ints. 3 | pub fn progressive_sum_ints() -> Int { 4 | let s = @host.input_string() 5 | let jv = try @json.parse!(s) catch { 6 | e => { 7 | @host.set_error("unable to parse input: \{e}") 8 | return 1 // failure 9 | } 10 | } 11 | // 12 | let ints = match jv.as_array() { 13 | Some(arr) => arr.map(fn(jv) { jv.as_number().unwrap().to_int() }) 14 | _ => { 15 | @host.set_error("could not parse array") 16 | return 1 // failure 17 | } 18 | } 19 | // 20 | let result = process_ints(ints) 21 | // 22 | let jv = result.to_json() 23 | @host.output_json_value(jv) 24 | 0 // success 25 | } 26 | 27 | ///| 28 | /// `progressive_sum_floats` tests passing arrays of floats. 29 | pub fn progressive_sum_floats() -> Int { 30 | let s = @host.input_string() 31 | let jv = try @json.parse!(s) catch { 32 | e => { 33 | @host.set_error("unable to parse input: \{e}") 34 | return 1 // failure 35 | } 36 | } 37 | // 38 | let floats = match jv.as_array() { 39 | Some(arr) => arr.map(fn(jv) { jv.as_number().unwrap() }) 40 | _ => { 41 | @host.set_error("could not parse array") 42 | return 1 // failure 43 | } 44 | } 45 | // 46 | let result = process_floats(floats) 47 | // 48 | let jv = result.to_json() 49 | @host.output_json_value(jv) 50 | 0 // success 51 | } 52 | 53 | ///| 54 | /// `progressive_concat_strings` tests passing arrays of strings. 55 | pub fn progressive_concat_strings() -> Int { 56 | let s = @host.input_string() 57 | let jv = try @json.parse!(s) catch { 58 | e => { 59 | @host.set_error("unable to parse input: \{e}") 60 | return 1 // failure 61 | } 62 | } 63 | // 64 | let strings = match jv.as_array() { 65 | Some(arr) => arr.map(fn(jv) { jv.as_string().unwrap() }) 66 | _ => { 67 | @host.set_error("could not parse array") 68 | return 1 // failure 69 | } 70 | } 71 | // 72 | let result = process_strings(strings) 73 | // 74 | let jv = result.to_json() 75 | @host.output_json_value(jv) 76 | 0 // success 77 | } 78 | 79 | ///| 80 | /// `all_three_object` tests passing an object of all three arrays. 81 | pub fn all_three_object() -> Int { 82 | let s = @host.input_string() 83 | let jv = try @json.parse!(s) catch { 84 | e => { 85 | @host.set_error("unable to parse input: \{e}") 86 | return 1 // failure 87 | } 88 | } 89 | // 90 | let all_three : AllThree = match jv { 91 | Object( 92 | { 93 | "ints": Array(ints), 94 | "floats": Array(floats), 95 | "strings": Array(strings), 96 | .. 97 | } 98 | ) => { 99 | let ints : Array[Int] = ints 100 | .map(fn { n => n.as_number() }) 101 | .filter(fn { n => not(n.is_empty()) }) 102 | .map(fn { n => n.unwrap().to_int() }) 103 | let floats : Array[Double] = floats 104 | .map(fn { n => n.as_number() }) 105 | .filter(fn { n => not(n.is_empty()) }) 106 | .map(fn { n => n.unwrap() }) 107 | let strings : Array[String] = strings 108 | .map(fn { n => n.as_string() }) 109 | .filter(fn { n => not(n.is_empty()) }) 110 | .map(fn { n => n.unwrap() }) 111 | { ints, floats, strings } 112 | } 113 | _ => { 114 | @host.set_error("unable to parse input: \{s}") 115 | return 1 // failure 116 | } 117 | } 118 | // 119 | let result = process_all_three(all_three) 120 | // 121 | let jv = result.to_json() 122 | @host.output_json_value(jv) 123 | 0 // success 124 | } 125 | -------------------------------------------------------------------------------- /examples/arrays/strings.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | /// `process_strings` concatenates an array of strings. 3 | pub fn process_strings(strings : Array[String]) -> Array[String] { 4 | let parts = [] 5 | strings.eachi(fn(index, value) { 6 | parts.push(value) 7 | strings[index] = parts.join("|") 8 | }) 9 | strings 10 | } 11 | -------------------------------------------------------------------------------- /examples/count-vowels/README.md: -------------------------------------------------------------------------------- 1 | # examples/count-vowels 2 | 3 | The `count-vowels.wasm` plugin can be run from the top-level of the repo by 4 | typing: 5 | 6 | ```bash 7 | $ ./build.sh 8 | $ ./scripts/python-server.sh 9 | ``` 10 | 11 | Then open your browser window to: 12 | http://localhost:8080/examples/count-vowels 13 | 14 | ![count-vowels demo](count-vowels-demo.png) 15 | -------------------------------------------------------------------------------- /examples/count-vowels/count-vowels-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extism/moonbit-pdk/2854ba28833e6c8e2836fd7351d21b699a5a6430/examples/count-vowels/count-vowels-demo.png -------------------------------------------------------------------------------- /examples/count-vowels/count-vowels.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | /// `default_vowels` represents the default set of vowels 3 | /// if the host provides no "config.vowels" string. 4 | let default_vowels = "aeiouAEIOU" 5 | 6 | ///| 7 | /// `VowelReport` represents the JSON struct returned to the host. 8 | pub(all) struct VowelReport { 9 | count : Int 10 | total : Int 11 | vowels : String 12 | } derive(Show, Eq, ToJson) 13 | 14 | ///| 15 | fn get_total() -> Int { 16 | @var.get_int("total").or_default() 17 | } 18 | 19 | ///| 20 | fn store_total(total : Int) -> Unit { 21 | @var.set_int("total", total) 22 | } 23 | 24 | ///| 25 | fn get_vowels() -> String { 26 | @config.get("vowels").or(default_vowels) 27 | } 28 | 29 | ///| 30 | /// `count_vowels` reads the input string from the host, reads the "vowels" 31 | /// config from the host, then counts the number of vowels in the input 32 | /// string and keeps a running total (over multiple iterations) 33 | /// in the host's "total" var. 34 | /// It sends the JSON `VowelReport` to the host via its output data channel. 35 | /// It returns 0 to the host on success. 36 | pub fn count_vowels() -> Int { 37 | let input = @host.input_string() 38 | // 39 | let vowels = get_vowels() 40 | let vowels_arr = vowels.to_array() 41 | let count = input.iter().filter(fn(ch) { vowels_arr.contains(ch) }).count() 42 | // 43 | let total = get_total() + count 44 | store_total(total) 45 | // 46 | { count, total, vowels }.to_json() |> @host.output_json_value() 47 | 0 // success 48 | } 49 | 50 | ///| 51 | test "VowelReport::to_json works as expected" { 52 | let vowel_report = { count: 1, total: 2, vowels: "some string" } 53 | let got = vowel_report.to_json().stringify() 54 | let want = 55 | #|{"count":1,"total":2,"vowels":"some string"} 56 | assert_eq!(got, want) 57 | } 58 | -------------------------------------------------------------------------------- /examples/count-vowels/count-vowels.mbti: -------------------------------------------------------------------------------- 1 | package "extism/moonbit-pdk/examples/count-vowels" 2 | 3 | // Values 4 | fn count_vowels() -> Int 5 | 6 | // Types and methods 7 | pub(all) struct VowelReport { 8 | count : Int 9 | total : Int 10 | vowels : String 11 | } 12 | impl Eq for VowelReport 13 | impl Show for VowelReport 14 | impl ToJson for VowelReport 15 | 16 | // Type aliases 17 | 18 | // Traits 19 | 20 | -------------------------------------------------------------------------------- /examples/count-vowels/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /examples/count-vowels/moon.pkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "import": [ 3 | "extism/moonbit-pdk/pdk/config", 4 | "extism/moonbit-pdk/pdk/host", 5 | "extism/moonbit-pdk/pdk/var" 6 | ], 7 | "link": { 8 | "wasm": { 9 | "exports": [ 10 | "count_vowels" 11 | ], 12 | "export-memory-name": "memory" 13 | }, 14 | "wasm-gc": { 15 | "exports": [ 16 | "count_vowels" 17 | ], 18 | "export-memory-name": "memory" 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /examples/greet/README.md: -------------------------------------------------------------------------------- 1 | # examples/greet 2 | 3 | The `greet.wasm` plugin can be run from the top-level of the repo by 4 | typing: 5 | 6 | ```bash 7 | $ ./build.sh 8 | $ ./scripts/python-server.sh 9 | ``` 10 | 11 | Then open your browser window to: 12 | http://localhost:8080/examples/greet 13 | 14 | ![greet demo](greet-demo.png) 15 | -------------------------------------------------------------------------------- /examples/greet/greet-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extism/moonbit-pdk/2854ba28833e6c8e2836fd7351d21b699a5a6430/examples/greet/greet-demo.png -------------------------------------------------------------------------------- /examples/greet/greet.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | /// `greet` reads the input string from the host and writes a 3 | /// greeting to the host's output string. 4 | /// It returns 0 to the host on success. 5 | pub fn greet() -> Int { 6 | let input = @host.input_string() 7 | let greeting = "Hello, \{input}!" 8 | @host.output_string(greeting) 9 | 0 // success 10 | } 11 | -------------------------------------------------------------------------------- /examples/greet/greet.mbti: -------------------------------------------------------------------------------- 1 | package "extism/moonbit-pdk/examples/greet" 2 | 3 | // Values 4 | fn greet() -> Int 5 | 6 | // Types and methods 7 | 8 | // Type aliases 9 | 10 | // Traits 11 | 12 | -------------------------------------------------------------------------------- /examples/greet/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /examples/greet/moon.pkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "import": [ 3 | "extism/moonbit-pdk/pdk/host" 4 | ], 5 | "link": { 6 | "wasm": { 7 | "exports": [ 8 | "greet" 9 | ], 10 | "export-memory-name": "memory" 11 | }, 12 | "wasm-gc": { 13 | "exports": [ 14 | "greet" 15 | ], 16 | "export-memory-name": "memory" 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /examples/http-get/README.md: -------------------------------------------------------------------------------- 1 | # examples/http-get 2 | 3 | The `http-get.wasm` plugin can be run from the top-level of the repo by 4 | typing: 5 | 6 | ```bash 7 | $ ./build.sh 8 | $ ./scripts/http-get.sh 9 | ``` 10 | -------------------------------------------------------------------------------- /examples/http-get/http-get.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | /// `http_get` makes a GET HTTP request through the Extism host, gets 3 | /// the response back, then sends it (unmodified) to the Extism host output. 4 | /// It returns 0 to the host on success. 5 | pub fn http_get() -> Int { 6 | // create an HTTP Request (without relying on WASI), set headers as needed 7 | let req = @http.new_request( 8 | @http.Method::GET, 9 | "https://jsonplaceholder.typicode.com/todos/1", 10 | ) 11 | req.header.set("some-name", "some-value") 12 | req.header.set("another", "again") 13 | // send the request, get response back 14 | let res = req.send() 15 | 16 | // zero-copy send output to host 17 | res.output() 18 | 0 // success 19 | } 20 | -------------------------------------------------------------------------------- /examples/http-get/http-get.mbti: -------------------------------------------------------------------------------- 1 | package "extism/moonbit-pdk/examples/http-get" 2 | 3 | // Values 4 | fn http_get() -> Int 5 | 6 | // Types and methods 7 | 8 | // Type aliases 9 | 10 | // Traits 11 | 12 | -------------------------------------------------------------------------------- /examples/http-get/moon.pkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "import": [ 3 | "extism/moonbit-pdk/pdk/http" 4 | ], 5 | "link": { 6 | "wasm": { 7 | "exports": [ 8 | "http_get" 9 | ], 10 | "export-memory-name": "memory" 11 | }, 12 | "wasm-gc": { 13 | "exports": [ 14 | "http_get" 15 | ], 16 | "export-memory-name": "memory" 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /examples/kitchen-sink/README.md: -------------------------------------------------------------------------------- 1 | # examples/kitchen-sink 2 | 3 | The `kitchen-sink.wasm` plugin can be run from the top-level of the repo by 4 | typing: 5 | 6 | ```bash 7 | $ ./build.sh 8 | $ ./scripts/kitchen-sink.sh 9 | ``` 10 | -------------------------------------------------------------------------------- /examples/kitchen-sink/kitchen-sink.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | pub fn kitchen_sink() -> Int { 3 | // Input 4 | let input = @host.input_string() 5 | 6 | // Config 7 | let _ = @config.get("test") 8 | 9 | // Vars 10 | @var.set_string("test_var", "something") 11 | let test_var = @var.get_string("test_var") 12 | if test_var != Some("something") { 13 | @host.log_error_str("Invalid test_var") 14 | } 15 | 16 | // HTTP 17 | let req = @http.new_request(@http.Method::GET, "https://extism.org") 18 | let resp = req.send() 19 | if resp.status_code != 200 { 20 | @host.log_error_str("Invalid HTTP status code") 21 | } 22 | 23 | // Logging 24 | @host.log_info_str("INFO") 25 | @host.log_debug_str("DEBUG") 26 | @host.log_warn_str("WARN") 27 | @host.log_error_str("ERROR") 28 | 29 | // Output 30 | @host.output_string(input) 31 | return 0 32 | } 33 | -------------------------------------------------------------------------------- /examples/kitchen-sink/kitchen-sink.mbti: -------------------------------------------------------------------------------- 1 | package "extism/moonbit-pdk/examples/kitchen-sink" 2 | 3 | // Values 4 | fn kitchen_sink() -> Int 5 | 6 | // Types and methods 7 | 8 | // Type aliases 9 | 10 | // Traits 11 | 12 | -------------------------------------------------------------------------------- /examples/kitchen-sink/moon.pkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "import": [ 3 | "extism/moonbit-pdk/pdk/config", 4 | "extism/moonbit-pdk/pdk/http", 5 | "extism/moonbit-pdk/pdk/host", 6 | "extism/moonbit-pdk/pdk/var" 7 | ], 8 | "link": { 9 | "wasm": { 10 | "exports": [ 11 | "kitchen_sink" 12 | ], 13 | "export-memory-name": "memory" 14 | }, 15 | "wasm-gc": { 16 | "exports": [ 17 | "kitchen_sink" 18 | ], 19 | "export-memory-name": "memory" 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extism/moonbit-pdk/2854ba28833e6c8e2836fd7351d21b699a5a6430/favicon.ico -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/extism/moonbit-pdk 2 | 3 | go 1.22.4 4 | 5 | require github.com/extism/go-sdk v1.2.0 6 | 7 | require ( 8 | github.com/gobwas/glob v0.2.3 // indirect 9 | github.com/tetratelabs/wazero v1.3.0 // indirect 10 | ) 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/extism/go-sdk v1.2.0 h1:A0DnIMthdP8h6K9NbRpRs1PIXHOUlb/t/TZWk5eUzx4= 4 | github.com/extism/go-sdk v1.2.0/go.mod h1:xUfKSEQndAvHBc1Ohdre0e+UdnRzUpVfbA8QLcx4fbY= 5 | github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= 6 | github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= 7 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 8 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 9 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 10 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 11 | github.com/tetratelabs/wazero v1.3.0 h1:nqw7zCldxE06B8zSZAY0ACrR9OH5QCcPwYmYlwtcwtE= 12 | github.com/tetratelabs/wazero v1.3.0/go.mod h1:wYx2gNRg8/WihJfSDxA1TIL8H+GkfLYm+bIfbblu9VQ= 13 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 14 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 15 | -------------------------------------------------------------------------------- /moon.mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "extism/moonbit-pdk", 3 | "version": "0.47.7", 4 | "deps": {}, 5 | "readme": "README.md", 6 | "repository": "https://github.com/extism/moonbit-pdk", 7 | "license": "Apache-2.0", 8 | "keywords": [ 9 | "extism", 10 | "pdk", 11 | "moonbit" 12 | ], 13 | "description": "Extism MoonBit PDK: This library can be used to write Extism Plug-ins in MoonBit." 14 | } -------------------------------------------------------------------------------- /pdk/config/config.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | /// `Config` provides methods to get "config" data from the host. 3 | pub(all) struct Config {} 4 | 5 | ///| 6 | /// `get_memory` returns a "config" Memory block from the host that is keyed by `key`. 7 | /// Note that no processing is performed on this block of memory. 8 | pub fn get_memory(key : String) -> @host.Memory? { 9 | let key_mem = @host.allocate_string(key) 10 | let offset = @extism.config_get(key_mem.offset) 11 | key_mem.free() 12 | if offset == 0L { 13 | return None 14 | } 15 | let length = @extism.length(offset) 16 | if length == 0L { 17 | return None 18 | } 19 | Some({ offset, length }) 20 | } 21 | 22 | ///| 23 | /// `get` returns a "config" String from the host that is keyed by `key`. 24 | /// Note that the Extism host strings are UTF-8 and therefore the returned 25 | /// String is encoded as UTF-16 in compliance with MoonBit Strings. 26 | pub fn get(key : String) -> String? { 27 | match get_memory(key) { 28 | Some(mem) => { 29 | let s = mem.to_string() 30 | mem.free() 31 | Some(s) 32 | } 33 | None => None 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /pdk/config/config.mbti: -------------------------------------------------------------------------------- 1 | package "extism/moonbit-pdk/pdk/config" 2 | 3 | import( 4 | "extism/moonbit-pdk/pdk/host" 5 | ) 6 | 7 | // Values 8 | fn get(String) -> String? 9 | 10 | fn get_memory(String) -> @host.Memory? 11 | 12 | // Types and methods 13 | pub(all) struct Config { 14 | } 15 | 16 | // Type aliases 17 | 18 | // Traits 19 | 20 | -------------------------------------------------------------------------------- /pdk/config/moon.pkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "import": [ 3 | "extism/moonbit-pdk/pdk/extism", 4 | "extism/moonbit-pdk/pdk/host" 5 | ] 6 | } -------------------------------------------------------------------------------- /pdk/extism/env.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | /// `input_length` returns the number of (unprocessed) bytes provided by the host via its input methods. 3 | /// The user of this PDK will typically not call this method directly. 4 | pub fn input_length() -> Int64 = "extism:host/env" "input_length" 5 | 6 | ///| 7 | /// `input_load_u8` returns the byte at location `offset` of the "input" data from the host. 8 | /// The user of this PDK will typically not call this method directly. 9 | pub fn input_load_u8(offset : Int64) -> Byte = "extism:host/env" "input_load_u8" 10 | 11 | ///| 12 | /// `input_load_u64` returns the 64-bit unsigned integer of the "input" data from the host. 13 | /// Note that MoonBit has no unsigned integers, 14 | /// so the result is returned as an Int64. 15 | /// Also note that `offset` must lie on an 8-byte boundary. 16 | /// The user of this PDK will typically not call this method directly. 17 | pub fn input_load_u64(offset : Int64) -> Int64 = "extism:host/env" "input_load_u64" 18 | 19 | ///| 20 | /// `length` returns the number of bytes associated with the block of host memory 21 | /// located at `offset`. 22 | /// The user of this PDK will typically not call this method directly. 23 | pub fn length(offset : Int64) -> Int64 = "extism:host/env" "length" 24 | 25 | ///| 26 | /// `alloc` allocates `length` bytes of data with host memory for use by the plugin 27 | /// and returns its `offset` within the host memory block. 28 | /// The user of this PDK will typically not call this method directly. 29 | pub fn alloc(length : Int64) -> Int64 = "extism:host/env" "alloc" 30 | 31 | ///| 32 | /// `free` releases the bytes previously allocated with `alloc` at the given `offset`. 33 | /// The user of this PDK will typically not call this method directly. 34 | pub fn free(offset : Int64) = "extism:host/env" "free" 35 | 36 | ///| 37 | /// `output_set` sets the "output" data from the plugin to the host to be the memory that 38 | /// has been written at `offset` with the given `length`. 39 | /// The user of this PDK will typically not call this method directly. 40 | pub fn output_set(offset : Int64, length : Int64) = "extism:host/env" "output_set" 41 | 42 | ///| 43 | /// `error_set` sets the "error" data from the plugin to the host to be the memory that 44 | /// has been written at `offset`. 45 | /// The user of this PDK will typically not call this method directly. 46 | pub fn error_set(offset : Int64) = "extism:host/env" "error_set" 47 | 48 | ///| 49 | /// `config_get` returns the host memory block offset for the "config" data associated with 50 | /// the key which is represented by the UTF-8 string which as been previously 51 | /// written at `offset`. 52 | /// The user of this PDK will typically not call this method directly. 53 | pub fn config_get(offset : Int64) -> Int64 = "extism:host/env" "config_get" 54 | 55 | ///| 56 | /// `var_get` returns the host memory block offset for the "var" data associated with 57 | /// the key which is represented by the UTF-8 string which as been previously 58 | /// written at `offset`. 59 | /// The user of this PDK will typically not call this method directly. 60 | pub fn var_get(offset : Int64) -> Int64 = "extism:host/env" "var_get" 61 | 62 | ///| 63 | /// `var_set` sets the host "var" memory keyed by the UTF-8 string located at `offset` 64 | /// to be the value which has been previously written at `value_offset`. 65 | /// The user of this PDK will typically not call this method directly. 66 | pub fn var_set(offset : Int64, value_offset : Int64) = "extism:host/env" "var_set" 67 | 68 | ///| 69 | /// `store_u8` stores the Byte `b` at location `offset` in the host memory block. 70 | /// The user of this PDK will typically not call this method directly. 71 | pub fn store_u8(offset : Int64, b : Byte) = "extism:host/env" "store_u8" 72 | 73 | ///| 74 | /// `load_u8` returns the Byte located at `offset` in the host memory block. 75 | /// The user of this PDK will typically not call this method directly. 76 | pub fn load_u8(offset : Int64) -> Byte = "extism:host/env" "load_u8" 77 | 78 | ///| 79 | /// `store_u64` stores the Int64 value `v` at location `offset` in the host memory block. 80 | /// Note that MoonBit does not have unsigned integers, but the host interprets 81 | /// the provided `v` value as an unsigned 64-bit integer. 82 | /// Also note that `offset` must lie on an 8-byte boundary. 83 | /// The user of this PDK will typically not call this method directly. 84 | pub fn store_u64(offset : Int64, v : Int64) = "extism:host/env" "store_u64" 85 | 86 | ///| 87 | /// `load_u64` returns the 64-bit unsigned integer at location `offset` in the host memory block. 88 | /// Note that MoonBit has no unsigned integers, 89 | /// so the result is returned as an Int64. 90 | /// Also note that `offset` must lie on an 8-byte boundary. 91 | /// The user of this PDK will typically not call this method directly. 92 | pub fn load_u64(offset : Int64) -> Int64 = "extism:host/env" "load_u64" 93 | 94 | ///| 95 | /// `http_request` sends the HTTP request to the Extism host and returns back the 96 | /// memory offset to the response body. 97 | pub fn http_request(req : Int64, body : Int64) -> Int64 = "extism:host/env" "http_request" 98 | 99 | ///| 100 | /// `http_status_code` returns the status code for the last-sent `http_request` call. 101 | pub fn http_status_code() -> Int = "extism:host/env" "http_status_code" 102 | 103 | ///| 104 | /// `log_warn` logs a "warning" string to the host from the previously-written UTF-8 string written to `offset`. 105 | /// The user of this PDK will typically not call this method directly. 106 | pub fn log_warn(offset : Int64) = "extism:host/env" "log_warn" 107 | 108 | ///| 109 | /// `log_info` logs an "info" string to the host from the previously-written UTF-8 string written to `offset`. 110 | /// The user of this PDK will typically not call this method directly. 111 | pub fn log_info(offset : Int64) = "extism:host/env" "log_info" 112 | 113 | ///| 114 | /// `log_debug` logs a "debug" string to the host from the previously-written UTF-8 string written to `offset`. 115 | /// The user of this PDK will typically not call this method directly. 116 | pub fn log_debug(offset : Int64) = "extism:host/env" "log_debug" 117 | 118 | ///| 119 | /// `log_error` logs an "error" string to the host from the previously-written UTF-8 string written to `offset`. 120 | /// The user of this PDK will typically not call this method directly. 121 | pub fn log_error(offset : Int64) = "extism:host/env" "log_error" 122 | -------------------------------------------------------------------------------- /pdk/extism/extism.mbti: -------------------------------------------------------------------------------- 1 | package "extism/moonbit-pdk/pdk/extism" 2 | 3 | // Values 4 | fn alloc(Int64) -> Int64 5 | 6 | fn config_get(Int64) -> Int64 7 | 8 | fn error_set(Int64) -> Unit 9 | 10 | fn free(Int64) -> Unit 11 | 12 | fn http_request(Int64, Int64) -> Int64 13 | 14 | fn http_status_code() -> Int 15 | 16 | fn input_length() -> Int64 17 | 18 | fn input_load_u64(Int64) -> Int64 19 | 20 | fn input_load_u8(Int64) -> Byte 21 | 22 | fn length(Int64) -> Int64 23 | 24 | fn load_u64(Int64) -> Int64 25 | 26 | fn load_u8(Int64) -> Byte 27 | 28 | fn log_debug(Int64) -> Unit 29 | 30 | fn log_error(Int64) -> Unit 31 | 32 | fn log_info(Int64) -> Unit 33 | 34 | fn log_warn(Int64) -> Unit 35 | 36 | fn output_set(Int64, Int64) -> Unit 37 | 38 | fn store_u64(Int64, Int64) -> Unit 39 | 40 | fn store_u8(Int64, Byte) -> Unit 41 | 42 | fn var_get(Int64) -> Int64 43 | 44 | fn var_set(Int64, Int64) -> Unit 45 | 46 | // Types and methods 47 | 48 | // Type aliases 49 | 50 | // Traits 51 | 52 | -------------------------------------------------------------------------------- /pdk/extism/moon.pkg.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /pdk/host/host.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | /// `input` returns a sequence of (unprocessed) bytes from the host. 3 | pub fn input() -> Bytes { 4 | let length = @extism.input_length().to_int() 5 | let value = FixedArray::makei(length, fn(i) { 6 | @extism.input_load_u8(i.to_int64()) 7 | }) 8 | Bytes::from_fixedarray(value) 9 | } 10 | 11 | ///| 12 | /// `input_string` returns a (UTF-16) String from the host. 13 | pub fn input_string() -> String { 14 | input() |> @pdk.ToUtf16::to_utf16() 15 | } 16 | 17 | // `output_bytes_to_memory` writes the (unprocessed) bytes to a Memory buffer on the host. 18 | ///| 19 | fn output_bytes_to_memory(b : Bytes) -> Memory { 20 | let length = b.length().to_int64() 21 | let offset = @extism.alloc(length) 22 | for i = 0; i < length.to_int(); i = i + 1 { 23 | @extism.store_u8(offset + i.to_int64(), b[i]) 24 | } 25 | { offset, length } 26 | } 27 | 28 | ///| 29 | /// `output` sends a sequence of (unprocessed) bytes to the host as the plugin's "output". 30 | pub fn output(b : Bytes) -> Unit { 31 | let mem = output_bytes_to_memory(b) 32 | @extism.output_set(mem.offset, mem.length) 33 | // 2024-07-11: There is currently an issue with the JS SDK where if memory is 34 | // freed in the next line, the host receives a null from the `await plugin.call` function. 35 | // mem.free() 36 | } 37 | 38 | ///| 39 | /// `output_string` converts a MoonBit String (UTF-16) to an Extism string (UTF-8) 40 | /// and sends it to the host. 41 | pub fn output_string(s : String) -> Unit { 42 | @pdk.ToUtf8::to_utf8(s) |> output() 43 | } 44 | 45 | ///| 46 | /// `output_json_value` converts a MoonBit `Json` to an Extism JSON string 47 | /// and sends it to the host. 48 | pub fn output_json_value(j : Json) -> Unit { 49 | j.stringify() |> output_string() 50 | } 51 | 52 | ///| 53 | fn set_error_bytes(b : Bytes) -> Unit { 54 | let mem = output_bytes_to_memory(b) 55 | @extism.error_set(mem.offset) 56 | // If the following line is uncommented, the Extism CLI fails with this example: 57 | // $ extism call target/wasm/release/build/main/main.wasm greet --input "Benjamin" --wasi 58 | // Error: Encountered an unknown error in call to Extism plugin function greet 59 | // However, with the line commented out, the Extism CLI responds correctly: 60 | // $ extism call target/wasm/release/build/main/main.wasm greet --input "Benjamin" --wasi 61 | // Error: Sorry, we don't greet Benjamins! 62 | // mem.free() 63 | } 64 | 65 | ///| 66 | /// `set_error` converts a MoonBit String (UTF-16) to an Extism string (UTF-8) 67 | /// and sends it to the host as its error output. 68 | pub fn set_error(s : String) -> Unit { 69 | @pdk.ToUtf8::to_utf8(s) |> set_error_bytes() 70 | } 71 | 72 | ///| 73 | /// `log_warn_str` is a helper function to log a warn string to the host. 74 | pub fn log_warn_str(s : String) -> Unit { 75 | let { offset, .. } = @pdk.ToUtf8::to_utf8(s) |> output_bytes_to_memory() 76 | @extism.log_warn(offset) 77 | @extism.free(offset) 78 | } 79 | 80 | ///| 81 | /// `log_info_str` is a helper function to log an info string to the host. 82 | pub fn log_info_str(s : String) -> Unit { 83 | let { offset, .. } = @pdk.ToUtf8::to_utf8(s) |> output_bytes_to_memory() 84 | @extism.log_info(offset) 85 | @extism.free(offset) 86 | } 87 | 88 | ///| 89 | /// `log_debug_str` is a helper function to log a debug string to the host. 90 | pub fn log_debug_str(s : String) -> Unit { 91 | let { offset, .. } = @pdk.ToUtf8::to_utf8(s) |> output_bytes_to_memory() 92 | @extism.log_debug(offset) 93 | @extism.free(offset) 94 | } 95 | 96 | ///| 97 | /// `log_error_str` is a helper function to log an error string to the host. 98 | pub fn log_error_str(s : String) -> Unit { 99 | let { offset, .. } = @pdk.ToUtf8::to_utf8(s) |> output_bytes_to_memory() 100 | @extism.log_error(offset) 101 | @extism.free(offset) 102 | } 103 | -------------------------------------------------------------------------------- /pdk/host/host.mbti: -------------------------------------------------------------------------------- 1 | package "extism/moonbit-pdk/pdk/host" 2 | 3 | // Values 4 | fn allocate(Int64) -> Memory 5 | 6 | fn allocate_bytes(Bytes) -> Memory 7 | 8 | fn allocate_json_value(Json) -> Memory 9 | 10 | fn allocate_string(String) -> Memory 11 | 12 | fn find_memory(Int64) -> Memory 13 | 14 | fn free(Memory) -> Unit 15 | 16 | fn input() -> Bytes 17 | 18 | fn input_string() -> String 19 | 20 | fn log_debug_str(String) -> Unit 21 | 22 | fn log_error_str(String) -> Unit 23 | 24 | fn log_info_str(String) -> Unit 25 | 26 | fn log_warn_str(String) -> Unit 27 | 28 | fn output(Bytes) -> Unit 29 | 30 | fn output_json_value(Json) -> Unit 31 | 32 | fn output_memory(Memory) -> Unit 33 | 34 | fn output_string(String) -> Unit 35 | 36 | fn set_error(String) -> Unit 37 | 38 | fn to_bytes(Memory) -> Bytes 39 | 40 | fn to_int(Memory) -> Int 41 | 42 | fn to_string(Memory) -> String 43 | 44 | // Types and methods 45 | pub(all) struct Memory { 46 | offset : Int64 47 | length : Int64 48 | } 49 | fn Memory::free(Self) -> Unit 50 | fn Memory::output_memory(Self) -> Unit 51 | fn Memory::to_bytes(Self) -> Bytes 52 | fn Memory::to_int(Self) -> Int 53 | fn Memory::to_string(Self) -> String 54 | 55 | // Type aliases 56 | 57 | // Traits 58 | 59 | -------------------------------------------------------------------------------- /pdk/host/memory.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | /// `Memory` represents memory allocated by (and shared with) the host. 3 | /// TODO: What kind of error checking needs to happen here? 4 | pub(all) struct Memory { 5 | offset : Int64 6 | length : Int64 7 | } 8 | 9 | ///| 10 | /// `find_memory` returns a `Memory` struct from an offset provided by the host. 11 | pub fn find_memory(offset : Int64) -> Memory { 12 | let length = @extism.length(offset) 13 | { offset, length } 14 | } 15 | 16 | ///| 17 | /// `free` releases this Memory from the host. 18 | pub fn free(self : Memory) -> Unit { 19 | @extism.free(self.offset) 20 | } 21 | 22 | ///| 23 | /// `allocate` allocates an uninitialized (determined by host) 24 | /// area of shared memory on the host. 25 | pub fn allocate(length : Int64) -> Memory { 26 | { offset: @extism.alloc(length), length } 27 | } 28 | 29 | ///| 30 | /// `allocate_bytes` allocates and initializes host memory 31 | /// with the provided (unprocessed) bytes. 32 | pub fn allocate_bytes(bytes : Bytes) -> Memory { 33 | let length = bytes.length().to_int64() 34 | let offset = @extism.alloc(length) 35 | for i in 0L.. Unit { 44 | @extism.output_set(self.offset, self.length) 45 | } 46 | 47 | ///| 48 | /// `allocate_string` allocates and initializes a UTF-8 string 49 | /// in host memory that is converted from this UTF-16 MoonBit String. 50 | pub fn allocate_string(s : String) -> Memory { 51 | @pdk.ToUtf8::to_utf8(s) |> allocate_bytes() 52 | } 53 | 54 | ///| 55 | /// `allocate_json_value` allocates and initializes a UTF-8 string 56 | /// in host memory that is converted from this `Json`. 57 | pub fn allocate_json_value(j : Json) -> Memory { 58 | j.stringify() |> allocate_string() 59 | } 60 | 61 | ///| 62 | /// `to_string` reads and converts the UTF-8 string residing in the host memory 63 | /// to a UTF-16 MoonBit String. 64 | pub fn to_string(self : Memory) -> String { 65 | self.to_bytes() |> @pdk.ToUtf16::to_utf16() 66 | } 67 | 68 | ///| 69 | /// `to_int` reads and converts the u32 residing in the host memory 70 | /// to a MoonBit Int. 71 | pub fn to_int(self : Memory) -> Int { 72 | let bytes = self.to_bytes() 73 | bytes[0].to_int() + 74 | (bytes[1].to_int() << 8) + 75 | (bytes[2].to_int() << 16) + 76 | (bytes[3].to_int() << 24) 77 | } 78 | 79 | ///| 80 | /// `to_bytes` reads the (unprocessed) bytes residing in the host memory 81 | /// to a MoonBit Bytes. 82 | pub fn to_bytes(self : Memory) -> Bytes { 83 | let bytes = FixedArray::makei(self.length.to_int(), fn(i) { 84 | @extism.load_u8(self.offset + i.to_int64()) 85 | }) 86 | Bytes::from_fixedarray(bytes) 87 | } 88 | -------------------------------------------------------------------------------- /pdk/host/moon.pkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "import": [ 3 | "extism/moonbit-pdk/pdk", 4 | "extism/moonbit-pdk/pdk/extism" 5 | ] 6 | } -------------------------------------------------------------------------------- /pdk/http/header.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | /// `Header` represents an HTTP Request header. 3 | /// Multiple values for a single key are not deduped. 4 | pub(all) type Header Map[String, String] derive(Show, Eq, ToJson) 5 | 6 | ///| 7 | /// `Header::new` returns a new Header. 8 | pub fn Header::new() -> Header { 9 | Header(Map::new()) 10 | } 11 | 12 | ///| 13 | /// `add` adds a value to a named (by `key`) header field. 14 | /// If the header key already exists, the value is appended after a comma. 15 | pub fn add(self : Header, key : String, value : String) -> Unit { 16 | match self._.get(key) { 17 | Some(v) => self._[key] = "\{v},\{value}" 18 | None => self._[key] = value 19 | } 20 | } 21 | 22 | ///| 23 | /// `set` overwrites a value to a named (by `key`) header field. 24 | pub fn set(self : Header, key : String, value : String) -> Unit { 25 | self._[key] = value 26 | } 27 | 28 | ///| 29 | test "Header::add" { 30 | let h = Header::new() 31 | h.add("key1", "one") 32 | h.add("key2", "one") 33 | h.add("key2", "two") 34 | h.add("key2", "two") 35 | assert_eq!(h._.get("nokey"), None) 36 | assert_eq!(h._.get("key1"), Some("one")) 37 | assert_eq!(h._.get("key2"), Some("one,two,two")) 38 | } 39 | 40 | ///| 41 | test "Header::to_json works as expected" { 42 | let header : Header = { "key1": "one", "key2": "two" } 43 | let got = header.to_json().stringify() 44 | let want = 45 | #|{"key1":"one","key2":"two"} 46 | assert_eq!(got, want) 47 | } 48 | -------------------------------------------------------------------------------- /pdk/http/http.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | /// `Request` represents an HTTP request made by the Extism host. 3 | pub(all) struct Request { 4 | http_method : Method 5 | header : Header 6 | url : String 7 | } derive(ToJson) 8 | 9 | ///| 10 | /// `Response` represents an HTTP response from the Extism host. 11 | pub(all) struct Response { 12 | status_code : Int 13 | body : @host.Memory 14 | } 15 | 16 | ///| 17 | /// `new_request` returns a new `Request` using the provided 18 | /// `method` and `url`. 19 | pub fn new_request(http_method : Method, url : String) -> Request { 20 | let header = Header::new() 21 | { http_method, header, url } 22 | } 23 | 24 | ///| 25 | /// `send` sends the `Request` to the host, waits for a response, 26 | /// and returns it to the caller. 27 | /// Note that the (optional) `body` is freed by this call. 28 | pub fn send(self : Request, body~ : @host.Memory? = None) -> Response { 29 | let meta_mem = self.to_json() |> @host.allocate_json_value() 30 | let body_memory_offset = match body { 31 | Some(v) => v.offset 32 | None => 0L 33 | } 34 | // 35 | let response_offset = @extism.http_request( 36 | meta_mem.offset, 37 | body_memory_offset, 38 | ) 39 | let response_length = @extism.length(response_offset) 40 | let status_code = @extism.http_status_code() 41 | // 42 | meta_mem.free() 43 | match body { 44 | Some(body_mem) => body_mem.free() 45 | None => () 46 | } 47 | // 48 | let response_body : @host.Memory = { 49 | offset: response_offset, 50 | length: response_length, 51 | } 52 | { status_code, body: response_body } 53 | } 54 | 55 | ///| 56 | /// `output` sends the (unprocessed) `Response` body to the Extism host "output". 57 | pub fn output(self : Response) -> Unit { 58 | self.body.output_memory() 59 | } 60 | 61 | ///| 62 | test "Request::to_json works as expected" { 63 | let request = { 64 | http_method: GET, 65 | header: { "key1": "one", "key2": "two" }, 66 | url: "https://example.com", 67 | } 68 | let got = request.to_json().stringify(escape_slash=false) 69 | let want = 70 | #|{"http_method":"GET","header":{"key1":"one","key2":"two"},"url":"https://example.com"} 71 | assert_eq!(got, want) 72 | } 73 | 74 | ///| 75 | test "Json::stringify works on strings" { 76 | let url = "https://example.com" 77 | let got = url.to_json().stringify(escape_slash=false) 78 | let want = 79 | #|"https://example.com" 80 | assert_eq!(got, want) 81 | } 82 | -------------------------------------------------------------------------------- /pdk/http/http.mbti: -------------------------------------------------------------------------------- 1 | package "extism/moonbit-pdk/pdk/http" 2 | 3 | import( 4 | "extism/moonbit-pdk/pdk/host" 5 | ) 6 | 7 | // Values 8 | fn add(Header, String, String) -> Unit 9 | 10 | fn new_request(Method, String) -> Request 11 | 12 | fn output(Response) -> Unit 13 | 14 | fn send(Request, body~ : @host.Memory? = ..) -> Response 15 | 16 | fn set(Header, String, String) -> Unit 17 | 18 | // Types and methods 19 | pub(all) type Header Map[String, String] 20 | fn Header::add(Self, String, String) -> Unit 21 | fn Header::new() -> Self 22 | fn Header::set(Self, String, String) -> Unit 23 | impl Eq for Header 24 | impl Show for Header 25 | impl ToJson for Header 26 | 27 | pub(all) enum Method { 28 | GET 29 | HEAD 30 | POST 31 | PUT 32 | DELETE 33 | CONNECT 34 | OPTIONS 35 | TRACE 36 | PATCH 37 | } 38 | impl Show for Method 39 | impl ToJson for Method 40 | 41 | pub(all) struct Request { 42 | http_method : Method 43 | header : Header 44 | url : String 45 | } 46 | fn Request::send(Self, body~ : @host.Memory? = ..) -> Response 47 | impl ToJson for Request 48 | 49 | pub(all) struct Response { 50 | status_code : Int 51 | body : @host.Memory 52 | } 53 | fn Response::output(Self) -> Unit 54 | 55 | // Type aliases 56 | 57 | // Traits 58 | 59 | -------------------------------------------------------------------------------- /pdk/http/method.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | /// `Method` represents an HTTP method. 3 | /// Descriptions are from: https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods 4 | pub(all) enum Method { 5 | // The GET method requests a representation of the specified resource. Requests using GET should only retrieve data. 6 | GET 7 | // The HEAD method asks for a response identical to a GET request, but without the response body. 8 | HEAD 9 | // The POST method submits an entity to the specified resource, often causing a change in state or side effects on the server. 10 | POST 11 | // The PUT method replaces all current representations of the target resource with the request payload. 12 | PUT 13 | // The DELETE method deletes the specified resource. 14 | DELETE 15 | // The CONNECT method establishes a tunnel to the server identified by the target resource. 16 | CONNECT 17 | // The OPTIONS method describes the communication options for the target resource. 18 | OPTIONS 19 | // The TRACE method performs a message loop-back test along the path to the target resource. 20 | TRACE 21 | // The PATCH method applies partial modifications to a resource. 22 | PATCH 23 | } derive(Show) 24 | 25 | ///| 26 | /// `Method::to_json` is required because `derive(ToJson)` generates `{"$tag":"GET"}` here instead of `"GET"`. 27 | pub impl ToJson for Method with to_json(self) { 28 | self.to_string().to_json() 29 | } 30 | -------------------------------------------------------------------------------- /pdk/http/method_test.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | test "Method::to_json works as expected" { 3 | let got = @http.GET.to_json().stringify() 4 | let want = 5 | #|"GET" 6 | assert_eq!(got, want) 7 | } 8 | -------------------------------------------------------------------------------- /pdk/http/moon.pkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "import": [ 3 | "extism/moonbit-pdk/pdk/extism", 4 | "extism/moonbit-pdk/pdk/host" 5 | ] 6 | } -------------------------------------------------------------------------------- /pdk/moon.pkg.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /pdk/pdk.mbti: -------------------------------------------------------------------------------- 1 | package "extism/moonbit-pdk/pdk" 2 | 3 | // Values 4 | 5 | // Types and methods 6 | 7 | // Type aliases 8 | 9 | // Traits 10 | pub(open) trait ToUtf16 { 11 | to_utf16(Self) -> String 12 | } 13 | impl ToUtf16 for Bytes 14 | 15 | pub(open) trait ToUtf8 { 16 | to_utf8(Self) -> Bytes 17 | } 18 | impl ToUtf8 for String 19 | 20 | -------------------------------------------------------------------------------- /pdk/string.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | /// ToUtf8 is a workaround since the standard library does not make 3 | /// it easy to write a standard UTF-8 string. 4 | /// https://github.com/moonbitlang/core/issues/484 5 | pub(open) trait ToUtf8 { 6 | to_utf8(Self) -> Bytes 7 | } 8 | 9 | ///| 10 | /// `to_utf8` converts the MoonBit (UTF-16) `String` to a UTF-8 encoded `Bytes`. 11 | pub impl ToUtf8 for String with to_utf8(s : String) -> Bytes { 12 | let chars = s.to_array() 13 | // first, allocate the maximum possible length of the UTF-8 "string": 14 | let arr = FixedArray::makei(4 * chars.length(), fn { _ => b'\x00' }) 15 | let mut len = 0 16 | for i = 0; i < chars.length(); i = i + 1 { 17 | len += arr.set_utf8_char(len, chars[i]) 18 | } 19 | Bytes::from_fixedarray(arr, len~) 20 | } 21 | 22 | ///| 23 | /// ToUtf16 is a workaround since the standard library does not make 24 | /// it easy to write a standard UTF-16 string from UTF-8. 25 | /// https://github.com/moonbitlang/core/issues/484 26 | pub(open) trait ToUtf16 { 27 | to_utf16(Self) -> String 28 | } 29 | 30 | ///| 31 | /// `to_utf16` converts a UTF-8 encoded `Bytes` to a MoonBit (UTF-16) `String`. 32 | pub impl ToUtf16 for Bytes with to_utf16(b : Bytes) -> String { 33 | // TODO: Make a real UTF-8 => UTF-16 converter. 34 | // https://github.com/moonbitlang/core/issues/484 35 | let length = b.length() 36 | let buf = @buffer.new(size_hint=2 * length) 37 | let mut i = 0 38 | while i < length { 39 | if (b[i] & 0xF0) == 0xF0 { 40 | let code = ((b[i].to_int() & 0x07) << 18) | 41 | ((b[i + 1].to_int() & 0x3F) << 12) | 42 | ((b[i + 2].to_int() & 0x3F) << 6) | 43 | (b[i + 3].to_int() & 0x3F) 44 | buf.write_char(Int::unsafe_to_char(code)) 45 | i += 4 46 | continue 47 | } 48 | if (b[i] & 0xE0) == 0xE0 { 49 | let code = ((b[i].to_int() & 0x0F) << 12) | 50 | ((b[i + 1].to_int() & 0x3F) << 6) | 51 | (b[i + 2].to_int() & 0x3F) 52 | buf.write_char(Int::unsafe_to_char(code)) 53 | i += 3 54 | continue 55 | } 56 | if (b[i] & 0xC0) == 0xC0 { 57 | let code = ((b[i].to_int() & 0x1F) << 6) | (b[i + 1].to_int() & 0x3F) 58 | buf.write_char(Int::unsafe_to_char(code)) 59 | i += 2 60 | continue 61 | } 62 | let code = b[i].to_int() & 0x7F 63 | buf.write_char(Int::unsafe_to_char(code)) 64 | i += 1 65 | } 66 | buf.contents().to_unchecked_string() 67 | } 68 | -------------------------------------------------------------------------------- /pdk/string_test.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | test "simple ASCII utf-8 and utf-16 conversions work" { 3 | let orig = "This is a simple ASCII string" 4 | let utf8 = @pdk.ToUtf8::to_utf8(orig) 5 | assert_eq!(utf8, b"This is a simple ASCII string") 6 | let got = @pdk.ToUtf16::to_utf16(utf8) 7 | assert_eq!(got, orig) 8 | } 9 | 10 | ///| 11 | test "non-trivial utf-8 and utf-16 conversions work" { 12 | let orig = "这是一个不平凡的 UTF-16 字符串" 13 | let utf8 = @pdk.ToUtf8::to_utf8(orig) 14 | assert_eq!( 15 | utf8, b"\xe8\xbf\x99\xe6\x98\xaf\xe4\xb8\x80\xe4\xb8\xaa\xe4\xb8\x8d\xe5\xb9\xb3\xe5\x87\xa1\xe7\x9a\x84\x20\x55\x54\x46\x2d\x31\x36\x20\xe5\xad\x97\xe7\xac\xa6\xe4\xb8\xb2", 16 | ) 17 | let got = @pdk.ToUtf16::to_utf16(utf8) 18 | assert_eq!(got, orig) 19 | } 20 | -------------------------------------------------------------------------------- /pdk/var/moon.pkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "import": [ 3 | "extism/moonbit-pdk/pdk/extism", 4 | "extism/moonbit-pdk/pdk/host" 5 | ] 6 | } -------------------------------------------------------------------------------- /pdk/var/var.mbt: -------------------------------------------------------------------------------- 1 | ///| 2 | /// `get_memory` returns the (unprocessed) host Memory block for the "var" data associated with 3 | /// the provided `key`. 4 | pub fn get_memory(key : String) -> @host.Memory? { 5 | let key_mem = @host.allocate_string(key) 6 | let offset = @extism.var_get(key_mem.offset) 7 | key_mem.free() 8 | if offset == 0L { 9 | return None 10 | } 11 | let length = @extism.length(offset) 12 | if length == 0L { 13 | return None 14 | } 15 | Some({ offset, length }) 16 | } 17 | 18 | ///| 19 | /// `get_bytes` returns the (unprocessed) host Memory block for the "var" data associated with 20 | /// the provided `key`. 21 | pub fn get_bytes(key : String) -> Bytes? { 22 | match get_memory(key) { 23 | Some(v) => Some(v.to_bytes()) 24 | None => None 25 | } 26 | } 27 | 28 | ///| 29 | /// `get_int` returns the host's "var" Int associated with the provided `key`. 30 | pub fn get_int(key : String) -> Int? { 31 | match get_memory(key) { 32 | Some(v) => Some(v.to_int()) 33 | None => None 34 | } 35 | } 36 | 37 | ///| 38 | /// `get_string` returns the host's "var" String associated with the provided `key`. 39 | /// Note that the Extism host string is UTF-8 and the string is converted to 40 | /// a MoonBit (UTF-16) String. 41 | pub fn get_string(key : String) -> String? { 42 | match get_memory(key) { 43 | Some(v) => Some(v.to_string()) 44 | None => None 45 | } 46 | } 47 | 48 | ///| 49 | /// `set_bytes` sets the (unprocessed) host Memory block for the "var" data associated with 50 | /// the provided `key`. 51 | pub fn set_bytes(key : String, value : Bytes) -> Unit { 52 | let key_mem = @host.allocate_string(key) 53 | let val_mem = @host.allocate_bytes(value) 54 | @extism.var_set(key_mem.offset, val_mem.offset) 55 | key_mem.free() 56 | val_mem.free() 57 | } 58 | 59 | ///| 60 | /// `set_int` sets the host's "var" Int associated with the provided `key`. 61 | pub fn set_int(key : String, value : Int) -> Unit { 62 | let key_mem = @host.allocate_string(key) 63 | let bytes = FixedArray::makei(4, fn(i) { 64 | (value >> (i * 8)).land(255).to_byte() 65 | }) 66 | let val_mem = @host.allocate_bytes(Bytes::from_fixedarray(bytes)) 67 | @extism.var_set(key_mem.offset, val_mem.offset) 68 | key_mem.free() 69 | val_mem.free() 70 | } 71 | 72 | ///| 73 | /// `set_string` sets the host Memory block for the "var" data associated with 74 | /// the provided `key`. 75 | /// Note that the MoonBit String is UTF-16 and the string is converted to 76 | /// a UTF-8 string for the Extism host. 77 | pub fn set_string(key : String, value : String) -> Unit { 78 | let key_mem = @host.allocate_string(key) 79 | let val_mem = @host.allocate_string(value) 80 | @extism.var_set(key_mem.offset, val_mem.offset) 81 | key_mem.free() 82 | val_mem.free() 83 | } 84 | 85 | ///| 86 | /// `remove` deletes the value in the host's "var" memory associated with the provided `key`. 87 | pub fn remove(key : String) -> Unit { 88 | let key_mem = @host.allocate_string(key) 89 | @extism.var_set(key_mem.offset, 0L) 90 | key_mem.free() 91 | } 92 | -------------------------------------------------------------------------------- /pdk/var/var.mbti: -------------------------------------------------------------------------------- 1 | package "extism/moonbit-pdk/pdk/var" 2 | 3 | import( 4 | "extism/moonbit-pdk/pdk/host" 5 | ) 6 | 7 | // Values 8 | fn get_bytes(String) -> Bytes? 9 | 10 | fn get_int(String) -> Int? 11 | 12 | fn get_memory(String) -> @host.Memory? 13 | 14 | fn get_string(String) -> String? 15 | 16 | fn remove(String) -> Unit 17 | 18 | fn set_bytes(String, Bytes) -> Unit 19 | 20 | fn set_int(String, Int) -> Unit 21 | 22 | fn set_string(String, String) -> Unit 23 | 24 | // Types and methods 25 | 26 | // Type aliases 27 | 28 | // Traits 29 | 30 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | ./scripts/add.sh '{"a": 20, "b": 21}' && echo && echo 3 | ./scripts/greet.sh Benjamin && echo && echo 4 | ./scripts/count-vowels.sh 'Once upon a dream' && echo && echo 5 | ./scripts/http-get.sh && echo && echo 6 | ./scripts/kitchen-sink.sh 'Testing the kitchen sink' && echo && echo 7 | ./scripts/arrays-ints.sh '[0,1,2,3,4,5,6]' && echo && echo 8 | ./scripts/arrays-floats.sh '[0,0.1,0.2,0.3,0.4,0.5,0.6]' && echo && echo 9 | ./scripts/arrays-strings.sh '["0","1","2","3","4","5","6"]' && echo && echo 10 | ./scripts/arrays-object.sh '{"ints":[0,1,2,3,4,5,6],"floats":[0,0.1,0.2,0.3,0.4,0.5,0.6],"strings":["0","1","2","3","4","5","6"]}' && echo && echo 11 | -------------------------------------------------------------------------------- /scripts/add.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -ex 2 | extism call target/wasm/release/build/examples/add/add.wasm add --wasi --input "$@" 3 | -------------------------------------------------------------------------------- /scripts/arrays-floats.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -ex 2 | extism call target/wasm/release/build/examples/arrays/arrays.wasm progressive_sum_floats --wasi --input "$@" -------------------------------------------------------------------------------- /scripts/arrays-ints.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -ex 2 | extism call target/wasm/release/build/examples/arrays/arrays.wasm progressive_sum_ints --wasi --input "$@" -------------------------------------------------------------------------------- /scripts/arrays-object.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -ex 2 | extism call target/wasm/release/build/examples/arrays/arrays.wasm all_three_object --wasi --input "$@" 3 | -------------------------------------------------------------------------------- /scripts/arrays-strings.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -ex 2 | extism call target/wasm/release/build/examples/arrays/arrays.wasm progressive_concat_strings --wasi --input "$@" 3 | -------------------------------------------------------------------------------- /scripts/bump-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -ex 2 | go run cmd/bump-version/main.go 3 | -------------------------------------------------------------------------------- /scripts/count-vowels.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -ex 2 | extism call target/wasm/release/build/examples/count-vowels/count-vowels.wasm count_vowels --wasi --input "$@" 3 | -------------------------------------------------------------------------------- /scripts/debug-count-vowels.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -ex 2 | dlv debug cmd/run-plugin/main.go -- --wasm target/wasm/release/build/examples/count-vowels/count-vowels.wasm 3 | -------------------------------------------------------------------------------- /scripts/go-run-count-vowels.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | go run cmd/run-plugin/main.go --wasm target/wasm/release/build/examples/count-vowels/count-vowels.wasm 3 | -------------------------------------------------------------------------------- /scripts/greet.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -ex 2 | extism call target/wasm/release/build/examples/greet/greet.wasm greet --wasi --input "$@" 3 | -------------------------------------------------------------------------------- /scripts/http-get.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -ex 2 | extism call \ 3 | target/wasm/release/build/examples/http-get/http-get.wasm \ 4 | http_get \ 5 | --wasi \ 6 | --allow-host='*.typicode.com' 7 | -------------------------------------------------------------------------------- /scripts/kitchen-sink.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -ex 2 | extism call \ 3 | target/wasm/release/build/examples/kitchen-sink/kitchen-sink.wasm \ 4 | kitchen_sink \ 5 | --wasi \ 6 | --allow-host='extism.org' \ 7 | --input "$@" 8 | -------------------------------------------------------------------------------- /scripts/python-server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | echo "Please visit in your browser: http://localhost:8080/examples/count-vowels or http://localhost:8080/examples/greet and open the DevTools console to see the output" 3 | python3 -m http.server 8080 4 | -------------------------------------------------------------------------------- /update.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -ex 2 | moon update && moon install && rm -rf target 3 | moon fmt && moon info 4 | moon test --target all 5 | --------------------------------------------------------------------------------