├── .github ├── CODEOWNERS ├── actions │ └── libextism │ │ └── action.yaml ├── dependabot.yml └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── Extism.sln ├── LICENSE ├── Makefile ├── README.md ├── docfx.json ├── images ├── favicon.ico └── logo.png ├── index.md ├── nuget ├── Extism.runtime.win.csproj └── runtimes │ └── expected.txt ├── samples ├── Extism.Sdk.FSharpSample │ ├── Extism.Sdk.FSharpSample.fsproj │ └── Program.fs └── Extism.Sdk.Sample │ ├── Extism.Sdk.Sample.csproj │ ├── Program.cs │ └── README.md ├── src ├── Directory.build.props └── Extism.Sdk │ ├── CurrentPlugin.cs │ ├── Extism.Sdk.csproj │ ├── ExtismException.cs │ ├── HostFunction.cs │ ├── LibExtism.cs │ ├── LogLevel.cs │ ├── Manifest.cs │ ├── Plugin.cs │ └── README.md ├── test ├── Extism.Sdk.Benchmarks │ ├── Extism.Sdk.Benchmarks.csproj │ └── Program.cs └── Extism.Sdk │ ├── BasicTests.cs │ ├── CompiledPluginTests.cs │ ├── Extism.Sdk.Tests.csproj │ ├── Helpers.cs │ ├── ManifestTests.cs │ └── data │ └── test.txt ├── toc.yml └── wasm ├── alloc.wasm ├── code-functions.wasm ├── code.wasm ├── config.wasm ├── exit.wasm ├── fail.wasm ├── float.wasm ├── fs.wasm ├── globals.wasm ├── host_memory.wasm ├── http.wasm ├── kitchensink.wasm ├── log.wasm ├── loop.wasm ├── sleep.wasm └── var.wasm /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @mhmd-azeez 2 | 3 | -------------------------------------------------------------------------------- /.github/actions/libextism/action.yaml: -------------------------------------------------------------------------------- 1 | on: [workflow_call] 2 | 3 | name: libextism 4 | 5 | inputs: 6 | gh-token: 7 | description: "A GitHub PAT" 8 | default: ${{ github.token }} 9 | 10 | inputs: 11 | prefix: 12 | description: 'Prefix for extism CLI' 13 | required: false 14 | default: '/usr/local' 15 | 16 | runs: 17 | using: composite 18 | steps: 19 | - uses: actions/checkout@v3 20 | with: 21 | repository: extism/cli 22 | path: .extism-cli 23 | - uses: ./.extism-cli/.github/actions/extism-cli 24 | - name: Install 25 | shell: bash 26 | run: sudo extism lib install --version git --prefix ${{ inputs.prefix }} --github-token ${{ inputs.gh-token }} -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "nuget" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | pull_request: 6 | branches: 7 | - main 8 | 9 | name: .NET CI 10 | 11 | jobs: 12 | test: 13 | name: Test .NET SDK 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, macos-latest, windows-latest] 18 | 19 | steps: 20 | - name: Checkout sources 21 | uses: actions/checkout@v2 22 | 23 | - name: Setup .NET Core SDK 24 | uses: actions/setup-dotnet@v1 25 | with: 26 | dotnet-version: 9.x 27 | 28 | - name: Run tests 29 | run: | 30 | make test 31 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - 'v*' 5 | 6 | name: Release .NET SDK 7 | 8 | jobs: 9 | release-sdks: 10 | name: release-dotnet 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | 16 | - name: Setup .NET Core SDK 17 | uses: actions/setup-dotnet@v3.0.3 18 | with: 19 | dotnet-version: 9.x 20 | 21 | - name: Test .NET Sdk 22 | run: | 23 | make test 24 | 25 | - name: Generate Docs 26 | run: | 27 | dotnet tool update -g docfx 28 | docfx ./docfx.json 29 | 30 | - name: Publish .NET Sdk 31 | env: 32 | NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} 33 | run: | 34 | make publish 35 | 36 | - name: Deploy 37 | uses: peaceiris/actions-gh-pages@v3 38 | with: 39 | github_token: ${{ secrets.GITHUB_TOKEN }} 40 | publish_dir: _site 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # Tye 66 | .tye/ 67 | 68 | # ASP.NET Scaffolding 69 | ScaffoldingReadMe.txt 70 | 71 | # StyleCop 72 | StyleCopReport.xml 73 | 74 | # Files built by Visual Studio 75 | *_i.c 76 | *_p.c 77 | *_h.h 78 | *.ilk 79 | *.meta 80 | *.obj 81 | *.iobj 82 | *.pch 83 | *.pdb 84 | *.ipdb 85 | *.pgc 86 | *.pgd 87 | *.rsp 88 | *.sbr 89 | *.tlb 90 | *.tli 91 | *.tlh 92 | *.tmp 93 | *.tmp_proj 94 | *_wpftmp.csproj 95 | *.log 96 | *.tlog 97 | *.vspscc 98 | *.vssscc 99 | .builds 100 | *.pidb 101 | *.svclog 102 | *.scc 103 | 104 | # Chutzpah Test files 105 | _Chutzpah* 106 | 107 | # Visual C++ cache files 108 | ipch/ 109 | *.aps 110 | *.ncb 111 | *.opendb 112 | *.opensdf 113 | *.sdf 114 | *.cachefile 115 | *.VC.db 116 | *.VC.VC.opendb 117 | 118 | # Visual Studio profiler 119 | *.psess 120 | *.vsp 121 | *.vspx 122 | *.sap 123 | 124 | # Visual Studio Trace Files 125 | *.e2e 126 | 127 | # TFS 2012 Local Workspace 128 | $tf/ 129 | 130 | # Guidance Automation Toolkit 131 | *.gpState 132 | 133 | # ReSharper is a .NET coding add-in 134 | _ReSharper*/ 135 | *.[Rr]e[Ss]harper 136 | *.DotSettings.user 137 | 138 | # TeamCity is a build add-in 139 | _TeamCity* 140 | 141 | # DotCover is a Code Coverage Tool 142 | *.dotCover 143 | 144 | # AxoCover is a Code Coverage Tool 145 | .axoCover/* 146 | !.axoCover/settings.json 147 | 148 | # Coverlet is a free, cross platform Code Coverage Tool 149 | coverage*.json 150 | coverage*.xml 151 | coverage*.info 152 | 153 | # Visual Studio code coverage results 154 | *.coverage 155 | *.coveragexml 156 | 157 | # NCrunch 158 | _NCrunch_* 159 | .*crunch*.local.xml 160 | nCrunchTemp_* 161 | 162 | # MightyMoose 163 | *.mm.* 164 | AutoTest.Net/ 165 | 166 | # Web workbench (sass) 167 | .sass-cache/ 168 | 169 | # Installshield output folder 170 | [Ee]xpress/ 171 | 172 | # DocProject is a documentation generator add-in 173 | DocProject/buildhelp/ 174 | DocProject/Help/*.HxT 175 | DocProject/Help/*.HxC 176 | DocProject/Help/*.hhc 177 | DocProject/Help/*.hhk 178 | DocProject/Help/*.hhp 179 | DocProject/Help/Html2 180 | DocProject/Help/html 181 | 182 | # Click-Once directory 183 | publish/ 184 | 185 | # Publish Web Output 186 | *.[Pp]ublish.xml 187 | *.azurePubxml 188 | # Note: Comment the next line if you want to checkin your web deploy settings, 189 | # but database connection strings (with potential passwords) will be unencrypted 190 | *.pubxml 191 | *.publishproj 192 | 193 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 194 | # checkin your Azure Web App publish settings, but sensitive information contained 195 | # in these scripts will be unencrypted 196 | PublishScripts/ 197 | 198 | # NuGet Packages 199 | *.nupkg 200 | # NuGet Symbol Packages 201 | *.snupkg 202 | # The packages folder can be ignored because of Package Restore 203 | **/[Pp]ackages/* 204 | # except build/, which is used as an MSBuild target. 205 | !**/[Pp]ackages/build/ 206 | # Uncomment if necessary however generally it will be regenerated when needed 207 | #!**/[Pp]ackages/repositories.config 208 | # NuGet v3's project.json files produces more ignorable files 209 | *.nuget.props 210 | *.nuget.targets 211 | 212 | # Microsoft Azure Build Output 213 | csx/ 214 | *.build.csdef 215 | 216 | # Microsoft Azure Emulator 217 | ecf/ 218 | rcf/ 219 | 220 | # Windows Store app package directories and files 221 | AppPackages/ 222 | BundleArtifacts/ 223 | Package.StoreAssociation.xml 224 | _pkginfo.txt 225 | *.appx 226 | *.appxbundle 227 | *.appxupload 228 | 229 | # Visual Studio cache files 230 | # files ending in .cache can be ignored 231 | *.[Cc]ache 232 | # but keep track of directories ending in .cache 233 | !?*.[Cc]ache/ 234 | 235 | # Others 236 | ClientBin/ 237 | ~$* 238 | *~ 239 | *.dbmdl 240 | *.dbproj.schemaview 241 | *.jfm 242 | *.pfx 243 | *.publishsettings 244 | orleans.codegen.cs 245 | 246 | # Including strong name files can present a security risk 247 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 248 | #*.snk 249 | 250 | # Since there are multiple workflows, uncomment next line to ignore bower_components 251 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 252 | #bower_components/ 253 | 254 | # RIA/Silverlight projects 255 | Generated_Code/ 256 | 257 | # Backup & report files from converting an old project file 258 | # to a newer Visual Studio version. Backup files are not needed, 259 | # because we have git ;-) 260 | _UpgradeReport_Files/ 261 | Backup*/ 262 | UpgradeLog*.XML 263 | UpgradeLog*.htm 264 | ServiceFabricBackup/ 265 | *.rptproj.bak 266 | 267 | # SQL Server files 268 | *.mdf 269 | *.ldf 270 | *.ndf 271 | 272 | # Business Intelligence projects 273 | *.rdl.data 274 | *.bim.layout 275 | *.bim_*.settings 276 | *.rptproj.rsuser 277 | *- [Bb]ackup.rdl 278 | *- [Bb]ackup ([0-9]).rdl 279 | *- [Bb]ackup ([0-9][0-9]).rdl 280 | 281 | # Microsoft Fakes 282 | FakesAssemblies/ 283 | 284 | # GhostDoc plugin setting file 285 | *.GhostDoc.xml 286 | 287 | # Node.js Tools for Visual Studio 288 | .ntvs_analysis.dat 289 | node_modules/ 290 | 291 | # Visual Studio 6 build log 292 | *.plg 293 | 294 | # Visual Studio 6 workspace options file 295 | *.opt 296 | 297 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 298 | *.vbw 299 | 300 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 301 | *.vbp 302 | 303 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 304 | *.dsw 305 | *.dsp 306 | 307 | # Visual Studio 6 technical files 308 | *.ncb 309 | *.aps 310 | 311 | # Visual Studio LightSwitch build output 312 | **/*.HTMLClient/GeneratedArtifacts 313 | **/*.DesktopClient/GeneratedArtifacts 314 | **/*.DesktopClient/ModelManifest.xml 315 | **/*.Server/GeneratedArtifacts 316 | **/*.Server/ModelManifest.xml 317 | _Pvt_Extensions 318 | 319 | # Paket dependency manager 320 | .paket/paket.exe 321 | paket-files/ 322 | 323 | # FAKE - F# Make 324 | .fake/ 325 | 326 | # CodeRush personal settings 327 | .cr/personal 328 | 329 | # Python Tools for Visual Studio (PTVS) 330 | __pycache__/ 331 | *.pyc 332 | 333 | # Cake - Uncomment if you are using it 334 | # tools/** 335 | # !tools/packages.config 336 | 337 | # Tabs Studio 338 | *.tss 339 | 340 | # Telerik's JustMock configuration file 341 | *.jmconfig 342 | 343 | # BizTalk build output 344 | *.btp.cs 345 | *.btm.cs 346 | *.odx.cs 347 | *.xsd.cs 348 | 349 | # OpenCover UI analysis results 350 | OpenCover/ 351 | 352 | # Azure Stream Analytics local run output 353 | ASALocalRun/ 354 | 355 | # MSBuild Binary and Structured Log 356 | *.binlog 357 | 358 | # NVidia Nsight GPU debugger configuration file 359 | *.nvuser 360 | 361 | # MFractors (Xamarin productivity tool) working folder 362 | .mfractor/ 363 | 364 | # Local History for Visual Studio 365 | .localhistory/ 366 | 367 | # Visual Studio History (VSHistory) files 368 | .vshistory/ 369 | 370 | # BeatPulse healthcheck temp database 371 | healthchecksdb 372 | 373 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 374 | MigrationBackup/ 375 | 376 | # Ionide (cross platform F# VS Code tools) working folder 377 | .ionide/ 378 | 379 | # Fody - auto-generated XML schema 380 | FodyWeavers.xsd 381 | 382 | # VS Code files for those working on multiple tools 383 | .vscode/* 384 | !.vscode/settings.json 385 | !.vscode/tasks.json 386 | !.vscode/launch.json 387 | !.vscode/extensions.json 388 | *.code-workspace 389 | 390 | # Local History for Visual Studio Code 391 | .history/ 392 | 393 | # Windows Installer files from build outputs 394 | *.cab 395 | *.msi 396 | *.msix 397 | *.msm 398 | *.msp 399 | 400 | # JetBrains Rider 401 | *.sln.iml 402 | 403 | ## 404 | ## Visual studio for Mac 405 | ## 406 | 407 | 408 | # globs 409 | Makefile.in 410 | *.userprefs 411 | *.usertasks 412 | config.make 413 | config.status 414 | aclocal.m4 415 | install-sh 416 | autom4te.cache/ 417 | *.tar.gz 418 | tarballs/ 419 | test-results/ 420 | 421 | # Mac bundle stuff 422 | *.dmg 423 | *.app 424 | 425 | # content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore 426 | # General 427 | .DS_Store 428 | .AppleDouble 429 | .LSOverride 430 | 431 | # Icon must end with two \r 432 | Icon 433 | 434 | 435 | # Thumbnails 436 | ._* 437 | 438 | # Files that might appear in the root of a volume 439 | .DocumentRevisions-V100 440 | .fseventsd 441 | .Spotlight-V100 442 | .TemporaryItems 443 | .Trashes 444 | .VolumeIcon.icns 445 | .com.apple.timemachine.donotpresent 446 | 447 | # Directories potentially created on remote AFP share 448 | .AppleDB 449 | .AppleDesktop 450 | Network Trash Folder 451 | Temporary Items 452 | .apdisk 453 | 454 | # content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore 455 | # Windows thumbnail cache files 456 | Thumbs.db 457 | ehthumbs.db 458 | ehthumbs_vista.db 459 | 460 | # Dump file 461 | *.stackdump 462 | 463 | # Folder config file 464 | [Dd]esktop.ini 465 | 466 | # Recycle Bin used on file shares 467 | $RECYCLE.BIN/ 468 | 469 | # Windows Installer files 470 | *.cab 471 | *.msi 472 | *.msix 473 | *.msm 474 | *.msp 475 | 476 | # Windows shortcuts 477 | *.lnk 478 | 479 | nuget/runtimes/win-x64.dll 480 | 481 | # DocFx 482 | _site 483 | api -------------------------------------------------------------------------------- /Extism.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.4.33110.190 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Extism.Sdk", "src\Extism.Sdk\Extism.Sdk.csproj", "{1FAA7B6E-249C-4E4C-AE7A-A493A9D24475}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Extism.Sdk.Tests", "test\Extism.Sdk\Extism.Sdk.Tests.csproj", "{DB440D61-C781-4C59-9223-9A79CC9FB4E7}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Extism.Sdk.Sample", "samples\Extism.Sdk.Sample\Extism.Sdk.Sample.csproj", "{2232E572-E8BA-46A1-AF31-E4168960DB75}" 11 | EndProject 12 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Extism.Sdk.FSharpSample", "samples\Extism.Sdk.FSharpSample\Extism.Sdk.FSharpSample.fsproj", "{FD564581-E6FA-4380-B5D0-A0423BBA05A9}" 13 | EndProject 14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Extism.Sdk.Benchmarks", "test\Extism.Sdk.Benchmarks\Extism.Sdk.Benchmarks.csproj", "{8F7C7762-2E72-40DA-9834-6A5CD6BDCDD3}" 15 | EndProject 16 | Global 17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 18 | Debug|Any CPU = Debug|Any CPU 19 | Release|Any CPU = Release|Any CPU 20 | EndGlobalSection 21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 22 | {1FAA7B6E-249C-4E4C-AE7A-A493A9D24475}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {1FAA7B6E-249C-4E4C-AE7A-A493A9D24475}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {1FAA7B6E-249C-4E4C-AE7A-A493A9D24475}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {1FAA7B6E-249C-4E4C-AE7A-A493A9D24475}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {DB440D61-C781-4C59-9223-9A79CC9FB4E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {DB440D61-C781-4C59-9223-9A79CC9FB4E7}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {DB440D61-C781-4C59-9223-9A79CC9FB4E7}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {DB440D61-C781-4C59-9223-9A79CC9FB4E7}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {2232E572-E8BA-46A1-AF31-E4168960DB75}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {2232E572-E8BA-46A1-AF31-E4168960DB75}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {2232E572-E8BA-46A1-AF31-E4168960DB75}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {2232E572-E8BA-46A1-AF31-E4168960DB75}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {FD564581-E6FA-4380-B5D0-A0423BBA05A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {FD564581-E6FA-4380-B5D0-A0423BBA05A9}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {FD564581-E6FA-4380-B5D0-A0423BBA05A9}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {FD564581-E6FA-4380-B5D0-A0423BBA05A9}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {8F7C7762-2E72-40DA-9834-6A5CD6BDCDD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {8F7C7762-2E72-40DA-9834-6A5CD6BDCDD3}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {8F7C7762-2E72-40DA-9834-6A5CD6BDCDD3}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {8F7C7762-2E72-40DA-9834-6A5CD6BDCDD3}.Release|Any CPU.Build.0 = Release|Any CPU 42 | EndGlobalSection 43 | GlobalSection(SolutionProperties) = preSolution 44 | HideSolutionNode = FALSE 45 | EndGlobalSection 46 | GlobalSection(ExtensibilityGlobals) = postSolution 47 | SolutionGuid = {2B6BF267-F2A5-4CB5-8DFD-F11CC8787E6B} 48 | EndGlobalSection 49 | EndGlobal 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 Dylibso, Inc. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | 3 | # The NUGET_API_KEY variable can be passed in as an argument or as an environment variable. 4 | # If it is passed in as an argument, it will take precedence over the environment variable. 5 | NUGET_API_KEY ?= $(shell env | grep NUGET_API_KEY) 6 | 7 | # set LD_LIBRARY_PATH when we are NOT on windows 8 | ifneq ($(OS),Windows_NT) 9 | export LD_LIBRARY_PATH=/usr/local/lib 10 | endif 11 | 12 | prepare: 13 | dotnet build 14 | 15 | test: prepare 16 | dotnet test 17 | 18 | clean: 19 | dotnet clean 20 | 21 | publish: clean prepare 22 | dotnet pack -c Release ./src/Extism.Sdk/Extism.Sdk.csproj 23 | dotnet nuget push --source https://api.nuget.org/v3/index.json ./src/Extism.Sdk/bin/Release/*.nupkg --api-key $(NUGET_API_KEY) 24 | 25 | format: 26 | dotnet format -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Extism .NET Host SDK 2 | 3 | This repo houses the .NET SDK for integrating with the [Extism](https://extism.org/) runtime. Install this library into your host .NET applications to run Extism plugins. 4 | 5 | ## Installation 6 | 7 | This library depends on the native Extism runtime, we provide [native runtime packages](https://www.nuget.org/packages/Extism.runtime.all) for all supported operating systems. You can install with: 8 | 9 | ``` 10 | dotnet add package Extism.runtime.all 11 | ``` 12 | 13 | Then, add the [Extism.Sdk NuGet package](https://www.nuget.org/packages/Extism.Sdk) to your project: 14 | 15 | ``` 16 | dotnet add package Extism.Sdk 17 | ``` 18 | 19 | ### PowerShell 20 | 21 | Open a PowerShell console and detect the Common Language Runtime (CLR) major version with the command 22 | ``` 23 | [System.Environment]::Version 24 | ``` 25 | 26 | Download the [Extism.Sdk NuGet package](https://www.nuget.org/packages/Extism.Sdk) and change the extension from nupkg to zip. Open the zip file and go into the lib folder. Choose the net folder in dependency of the CLR major version and open it. Copy the file Extism.sdk.dll in your PowerShell script directory. 27 | 28 | Download the [Extism native runtime package](https://www.nuget.org/packages/Extism.runtime.all#dependencies-body-tab) in dependency of your operating system and change the extension from nupkg to zip. Open the zip file and go into the runtimes folder. At the end of the path you will find a file with the name libextism.so (shared object) or extism.dll (dynamic link library). Copy this file in your PowerShell script directory. 29 | 30 | ## Getting Started 31 | 32 | This guide should walk you through some of the concepts in Extism and this .NET library. 33 | 34 | First you should add a using statement for Extism: 35 | 36 | C#: 37 | ```csharp 38 | using System; 39 | 40 | using Extism.Sdk; 41 | ``` 42 | 43 | F#: 44 | ```fsharp 45 | open System 46 | 47 | open Extism.Sdk 48 | ``` 49 | 50 | ### PowerShell 51 | ```powershell 52 | [System.String]$LibDir = $($PSScriptRoot) 53 | [System.String]$Extism = $($LibDir) + "/Extism.Sdk.dll" 54 | Add-Type -Path $Extism 55 | ``` 56 | 57 | ## Creating A Plug-in 58 | 59 | The primary concept in Extism is the [plug-in](https://extism.org/docs/concepts/plug-in). You can think of a plug-in as a code module stored in a `.wasm` file. 60 | 61 | Since you may not have an Extism plug-in on hand to test, let's load a demo plug-in from the web: 62 | 63 | C#: 64 | ```csharp 65 | var manifest = new Manifest(new UrlWasmSource("https://github.com/extism/plugins/releases/latest/download/count_vowels.wasm")); 66 | 67 | using var plugin = new Plugin(manifest, new HostFunction[] { }, withWasi: true); 68 | ``` 69 | 70 | F#: 71 | ```fsharp 72 | let uri = Uri("https://github.com/extism/plugins/releases/latest/download/count_vowels.wasm") 73 | let manifest = Manifest(new UrlWasmSource(uri)) 74 | 75 | let plugin = new Plugin(manifest, Array.Empty(), withWasi = true) 76 | ``` 77 | 78 | PowerShell: 79 | ```powershell 80 | $Manifest = [Extism.Sdk.Manifest]::new( 81 | [Extism.Sdk.UrlWasmSource]::new( 82 | "https://github.com/extism/plugins/releases/latest/download/count_vowels.wasm" 83 | ) 84 | ) 85 | 86 | $HostFunctionArray = [Extism.Sdk.HostFunction[]]::new(0) 87 | 88 | $Options = [Extism.Sdk.PluginIntializationOptions]::new() 89 | $Options.WithWasi = $True 90 | 91 | $Plugin = [Extism.Sdk.Plugin]::new($Manifest, $HostFunctionArray, $Options) 92 | ``` 93 | 94 | > **Note**: The schema for this manifest can be found here: https://extism.org/docs/concepts/manifest/ 95 | 96 | ### Calling A Plug-in's Exports 97 | 98 | This plug-in was written in Rust and it does one thing, it counts vowels in a string. As such, it exposes one "export" function: `count_vowels`. We can call exports using `Plugin.Call`: 99 | 100 | C#: 101 | ```csharp 102 | var output = plugin.Call("count_vowels", "Hello, World!"); 103 | Console.WriteLine(output); 104 | // => {"count": 3, "total": 3, "vowels": "aeiouAEIOU"} 105 | ``` 106 | 107 | F#: 108 | ```fsharp 109 | let output = plugin.Call("count_vowels", "Hello, World!") 110 | printfn "%s" output 111 | // => {"count": 3, "total": 3, "vowels": "aeiouAEIOU"} 112 | ``` 113 | 114 | PowerShell: 115 | ```powershell 116 | $output = $Plugin.Call("count_vowels", "Hello, World!") 117 | Write-Host $output 118 | # => {"count": 3, "total": 3, "vowels": "aeiouAEIOU"} 119 | ``` 120 | 121 | All exports have a simple interface of optional bytes in, and optional bytes out. This plug-in happens to take a string and return a JSON encoded string with a report of results. 122 | 123 | ## Precompiling plugins 124 | 125 | If you're going to create more than one instance of the same plugin, we recommend pre-compiling the plugin and instantiate them: 126 | 127 | C#: 128 | 129 | ```csharp 130 | var manifest = new Manifest(new PathWasmSource("/path/to/plugin.wasm"), "main")); 131 | 132 | // pre-compile the wasm file 133 | using var compiledPlugin = new CompiledPlugin(_manifest, [], withWasi: true); 134 | 135 | // instantiate plugins 136 | using var plugin = compiledPlugin.Instantiate(); 137 | ``` 138 | 139 | F#: 140 | 141 | ```fsharp 142 | // Create manifest 143 | let manifest = Manifest(PathWasmSource("/path/to/plugin.wasm")) 144 | 145 | // Pre-compile the wasm file 146 | use compiledPlugin = new CompiledPlugin(manifest, Array.empty, withWasi = true) 147 | 148 | // Instantiate plugins 149 | use plugin = compiledPlugin.Instantiate() 150 | ``` 151 | 152 | This can have a dramatic effect on performance*: 153 | 154 | ``` 155 | // * Summary * 156 | 157 | BenchmarkDotNet v0.14.0, Windows 11 (10.0.22631.4460/23H2/2023Update/SunValley3) 158 | 13th Gen Intel Core i7-1365U, 1 CPU, 12 logical and 10 physical cores 159 | .NET SDK 9.0.100 160 | [Host] : .NET 9.0.0 (9.0.24.52809), X64 RyuJIT AVX2 161 | DefaultJob : .NET 9.0.0 (9.0.24.52809), X64 RyuJIT AVX2 162 | 163 | 164 | | Method | Mean | Error | StdDev | 165 | |-------------------------- |------------:|----------:|------------:| 166 | | CompiledPluginInstantiate | 266.2 ms | 6.66 ms | 19.11 ms | 167 | | PluginInstantiate | 27,592.4 ms | 635.90 ms | 1,783.12 ms | 168 | ``` 169 | 170 | *: See [the complete benchmark](./test/Extism.Sdk.Benchmarks/Program.cs) 171 | 172 | ### Plug-in State 173 | 174 | Plug-ins may be stateful or stateless. Plug-ins can maintain state b/w calls by the use of variables. Our count vowels plug-in remembers the total number of vowels it's ever counted in the "total" key in the result. You can see this by making subsequent calls to the export: 175 | 176 | C#: 177 | ```csharp 178 | var output = plugin.Call("count_vowels", "Hello, World!"); 179 | Console.WriteLine(output); 180 | // => {"count": 3, "total": 6, "vowels": "aeiouAEIOU"} 181 | 182 | output = plugin.Call("count_vowels", "Hello, World!"); 183 | Console.WriteLine(output); 184 | // => {"count": 3, "total": 9, "vowels": "aeiouAEIOU"} 185 | ``` 186 | 187 | F#: 188 | ```fsharp 189 | let output1 = plugin.Call("count_vowels", "Hello, World!") 190 | printfn "%s" output1 191 | // => {"count": 3, "total": 6, "vowels": "aeiouAEIOU"} 192 | 193 | let output2 = plugin.Call("count_vowels", "Hello, World!") 194 | printfn "%s" output2 195 | // => {"count": 3, "total": 9, "vowels": "aeiouAEIOU"} 196 | ``` 197 | 198 | These variables will persist until this plug-in is freed or you initialize a new one. 199 | 200 | ### Configuration 201 | 202 | Plug-ins may optionally take a configuration object. This is a static way to configure the plug-in. Our count-vowels plugin takes an optional configuration to change out which characters are considered vowels. Example: 203 | 204 | C#: 205 | ```csharp 206 | var manifest = new Manifest(new UrlWasmSource("")); 207 | 208 | using var plugin = new Plugin(manifest, new HostFunction[] { }, withWasi: true); 209 | 210 | var output = plugin.Call("count_vowels", "Yellow, World!"); 211 | Console.WriteLine(output); 212 | // => {"count": 3, "total": 3, "vowels": "aeiouAEIOU"} 213 | 214 | manifest = new Manifest(new UrlWasmSource("")) 215 | { 216 | Config = new Dictionary 217 | { 218 | { "vowels", "aeiouyAEIOUY" } 219 | }, 220 | }; 221 | 222 | using var plugin2 = new Plugin(manifest, new HostFunction[] { }, withWasi: true); 223 | 224 | var output2 = plugin2.Call("count_vowels", "Yellow, World!"); 225 | Console.WriteLine(output2); 226 | // => {"count": 4, "total": 4, "vowels": "aeiouAEIOUY"} 227 | ``` 228 | 229 | F#: 230 | ```fsharp 231 | let uri = Uri("https://github.com/extism/plugins/releases/latest/download/count_vowels.wasm") 232 | let manifest = Manifest(new UrlWasmSource(uri)) 233 | manifest.Config <- dict [("vowels", "aeiouAEIOU")] 234 | 235 | let plugin = new Plugin(manifest, Array.Empty(), withWasi = true) 236 | 237 | let output = plugin.Call("count_vowels", "Yellow, World!") 238 | Console.WriteLine(output) 239 | // => {"count": 3, "total": 3, "vowels": "aeiouAEIOU"} 240 | 241 | let manifest2 = 242 | Manifest(new UrlWasmSource(Uri("https://github.com/extism/plugins/releases/latest/download/count_vowels.wasm"))) 243 | manifest2.Config <- dict [("vowels", "aeiouyAEIOUY")] 244 | 245 | let plugin2 = 246 | new Plugin(manifest2, Array.Empty(), withWasi = true) 247 | 248 | let output2 = plugin2.Call("count_vowels", "Yellow, World!") 249 | printfn "%s" output2 250 | // => {"count": 4, "total": 4, "vowels": "aeiouAEIOUY"} 251 | ``` 252 | 253 | ### Host Functions 254 | 255 | Let's extend our count-vowels example a little bit: Instead of storing the `total` in an ephemeral plug-in var, let's store it in a persistent key-value store! 256 | 257 | Wasm can't use our KV store on it's own. This is where `Host Functions` come in. 258 | 259 | [Host functions](https://extism.org/docs/concepts/host-functions) allow us to grant new capabilities to our plug-ins from our application. They are simply some Go functions you write which can be passed down and invoked from any language inside the plug-in. 260 | 261 | Let's load the manifest like usual but load up this `count_vowels_kvstore` plug-in: 262 | 263 | C#: 264 | ```csharp 265 | var manifest = new Manifest(new UrlWasmSource("https://github.com/extism/plugins/releases/latest/download/count_vowels_kvstore.wasm")); 266 | ``` 267 | 268 | F#: 269 | ```fsharp 270 | let manifest = Manifest(new UrlWasmSource(Uri("https://github.com/extism/plugins/releases/latest/download/count_vowels_kvstore.wasm"))) 271 | ``` 272 | 273 | > *Note*: The source code for this is [here](https://github.com/extism/plugins/blob/main/count_vowels_kvstore/src/lib.rs) and is written in rust, but it could be written in any of our PDK languages. 274 | 275 | Unlike our previous plug-in, this plug-in expects you to provide host functions that satisfy our its import interface for a KV store. 276 | 277 | We want to expose two functions to our plugin, `void kv_write(key string, value byte[])` which writes a bytes value to a key and `byte[] kv_read(key string)` which reads the bytes at the given `key`. 278 | 279 | C#: 280 | ```csharp 281 | // pretend this is Redis or something :) 282 | var kvStore = new Dictionary(); 283 | 284 | var functions = new[] 285 | { 286 | HostFunction.FromMethod("kv_read", null, (CurrentPlugin plugin, long keyOffset) => 287 | { 288 | var key = plugin.ReadString(keyOffset); 289 | if (!kvStore.TryGetValue(key, out var value)) 290 | { 291 | value = new byte[] { 0, 0, 0, 0 }; 292 | } 293 | 294 | Console.WriteLine($"Read {BitConverter.ToUInt32(value)} from key={key}"); 295 | return plugin.WriteBytes(value); 296 | }), 297 | 298 | HostFunction.FromMethod("kv_write", null, (CurrentPlugin plugin, long keyOffset, long valueOffset) => 299 | { 300 | var key = plugin.ReadString(keyOffset); 301 | var value = plugin.ReadBytes(valueOffset); 302 | 303 | Console.WriteLine($"Writing value={BitConverter.ToUInt32(value)} from key={key}"); 304 | kvStore[key] = value.ToArray(); 305 | }) 306 | }; 307 | ``` 308 | 309 | F#: 310 | ```fsharp 311 | let kvStore = new Dictionary() 312 | 313 | let functions = 314 | [| 315 | HostFunction.FromMethod("kv_read", null, fun (plugin: CurrentPlugin) (offs: int64) -> 316 | let key = plugin.ReadString(offs) 317 | let value = 318 | match kvStore.TryGetValue(key) with 319 | | true, v -> v 320 | | _ -> [| 0uy; 0uy; 0uy; 0uy |] // Default value if key not found 321 | 322 | Console.WriteLine($"Read {BitConverter.ToUInt32(value, 0)} from key={key}") 323 | plugin.WriteBytes(value) 324 | ) 325 | 326 | HostFunction.FromMethod("kv_write", null, fun (plugin: CurrentPlugin) (kOffs: int64) (vOffs: int64) -> 327 | let key = plugin.ReadString(kOffs) 328 | let value = plugin.ReadBytes(vOffs).ToArray() 329 | 330 | Console.WriteLine($"Writing value={BitConverter.ToUInt32(value, 0)} from key={key}") 331 | kvStore.[key] <- value 332 | ) 333 | |] 334 | ``` 335 | 336 | > *Note*: In order to write host functions you should get familiar with the methods on the CurrentPlugin type. The `plugin` parameter is an instance of this type. 337 | 338 | We need to pass these imports to the plug-in to create them. All imports of a plug-in must be satisfied for it to be initialized: 339 | 340 | C#: 341 | ```csharp 342 | using var plugin = new Plugin(manifest, functions, withWasi: true); 343 | 344 | var output = plugin.Call("count_vowels", "Hello World!"); 345 | 346 | Console.WriteLine(output); 347 | // => Read 0 from key=count-vowels" 348 | // => Writing value=3 from key=count-vowels" 349 | // => {"count": 3, "total": 3, "vowels": "aeiouAEIOU"} 350 | 351 | output = plugin.Call("count_vowels", "Hello World!"); 352 | 353 | Console.WriteLine(output); 354 | // => Read 3 from key=count-vowels" 355 | // => Writing value=6 from key=count-vowels" 356 | // => {"count": 3, "total": 6, "vowels": "aeiouAEIOU"} 357 | ``` 358 | 359 | F#: 360 | ```fsharp 361 | let plugin = new Plugin(manifest, functions, withWasi = true) 362 | 363 | let output = plugin.Call("count_vowels", "Hello World!") 364 | printfn "%s" output 365 | // => Read 0 from key=count-vowels 366 | // => Writing value=3 from key=count-vowels 367 | // => {"count": 3, "total": 3, "vowels": "aeiouAEIOU"} 368 | 369 | let output2 = plugin.Call("count_vowels", "Hello World!") 370 | printfn "%s" output2 371 | // => Read 3 from key=count-vowels 372 | // => Writing value=6 from key=count-vowels 373 | // => {"count": 3, "total": 6, "vowels": "aeiouAEIOU"} 374 | ``` 375 | 376 | ## Passing context to host functions 377 | 378 | Extism provides two ways to pass context to host functions: 379 | 380 | ### UserData 381 | UserData allows you to associate persistent state with a host function that remains available across all calls to that function. This is useful for maintaining configuration or state that should be available throughout the lifetime of the host function. 382 | 383 | C#: 384 | 385 | ```csharp 386 | var hostFunc = new HostFunction( 387 | "hello_world", 388 | new[] { ExtismValType.PTR }, 389 | new[] { ExtismValType.PTR }, 390 | "Hello again!", // <= userData, this can be any .NET object 391 | (CurrentPlugin plugin, Span inputs, Span outputs) => { 392 | var text = plugin.GetUserData(); // <= We're retrieving the data back 393 | // Use text... 394 | }); 395 | ``` 396 | 397 | F#: 398 | 399 | ```fsharp 400 | // Create host function with userData 401 | let hostFunc = new HostFunction( 402 | "hello_world", 403 | [| ExtismValType.PTR |], 404 | [| ExtismValType.PTR |], 405 | "Hello again!", // userData can be any .NET object 406 | (fun (plugin: CurrentPlugin) (inputs: Span) (outputs: Span) -> 407 | // Retrieve the userData 408 | let text = plugin.GetUserData() 409 | printfn "%s" text // Prints: "Hello again!" 410 | // Rest of function implementation... 411 | )) 412 | ``` 413 | 414 | The userData object is preserved for the lifetime of the host function and can be retrieved in any call using `CurrentPlugin.GetUserData()`. If no userData was provided, `GetUserData()` will return the default value for type `T`. 415 | 416 | ### Call Host Context 417 | 418 | Call Host Context provides a way to pass per-call context data when invoking a plugin function. This is useful when you need to provide data specific to a particular function call rather than data that persists across all calls. 419 | 420 | C#: 421 | 422 | ```csharp 423 | // Pass context for specific call 424 | var context = new Dictionary { { "requestId", 42 } }; 425 | var result = plugin.CallWithHostContext("function_name", inputData, context); 426 | 427 | // Access in host function 428 | void HostFunction(CurrentPlugin plugin, Span inputs, Span outputs) 429 | { 430 | var context = plugin.GetCallHostContext>(); 431 | // Use context... 432 | } 433 | ``` 434 | 435 | F#: 436 | 437 | ```fsharp 438 | // Create context for specific call 439 | let context = dict [ "requestId", box 42 ] 440 | 441 | // Call plugin with context 442 | let result = plugin.CallWithHostContext("function_name", inputData, context) 443 | 444 | // Access context in host function 445 | let hostFunction (plugin: CurrentPlugin) (inputs: Span) (outputs: Span) = 446 | match plugin.GetCallHostContext>() with 447 | | null -> printfn "No context available" 448 | | context -> 449 | let requestId = context.["requestId"] :?> int 450 | printfn "Request ID: %d" requestId 451 | ``` 452 | 453 | Host context is only available for the duration of the specific function call and can be retrieved using `CurrentPlugin.GetHostContext()`. If no context was provided for the call, `GetHostContext()` will return the default value for type `T`. 454 | 455 | ## Fuel limit 456 | 457 | The fuel limit feature allows you to constrain plugin execution by limiting the number of instructions it can execute. This provides a safeguard against infinite loops or excessive resource consumption. 458 | 459 | ### Setting a fuel limit 460 | 461 | Set the fuel limit when initializing a plugin: 462 | 463 | C#: 464 | 465 | ```csharp 466 | var manifest = new Manifest(...); 467 | var options = new PluginIntializationOptions { 468 | FuelLimit = 1000, // plugin can execute 1000 instructions 469 | WithWasi = true 470 | }; 471 | 472 | var plugin = new Plugin(manifest, functions, options); 473 | ``` 474 | 475 | F#: 476 | 477 | ```fsharp 478 | let manifest = Manifest(PathWasmSource("/path/to/plugin.wasm")) 479 | let options = PluginIntializationOptions( 480 | FuelLimit = Nullable(1000L), // plugin can execute 1000 instructions 481 | WithWasi = true 482 | ) 483 | 484 | use plugin = new Plugin(manifest, Array.empty, options) 485 | ``` 486 | 487 | When the fuel limit is exceeded, the plugin execution is terminated and an `ExtismException` is thrown containing "fuel" in the error message. 488 | -------------------------------------------------------------------------------- /docfx.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": [ 3 | { 4 | "src": [ 5 | { 6 | "src": "src", 7 | "files": [ 8 | "**/*.csproj" 9 | ] 10 | } 11 | ], 12 | "dest": "api" 13 | } 14 | ], 15 | "build": { 16 | "content": [ 17 | { 18 | "files": [ 19 | "**/*.{md,yml}" 20 | ], 21 | "exclude": [ 22 | "_site/**" 23 | ] 24 | } 25 | ], 26 | "resource": [ 27 | { 28 | "files": [ 29 | "images/**" 30 | ] 31 | } 32 | ], 33 | "output": "_site", 34 | "template": [ 35 | "default", 36 | "modern" 37 | ], 38 | "globalMetadata": { 39 | "_appName": "Extism .NET SDK", 40 | "_appTitle": "Extism .NET SDK", 41 | "_enableSearch": true, 42 | "_appFaviconPath": "images/favicon.ico", 43 | "_appLogoPath": "images/logo.png", 44 | "pdf": true 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extism/dotnet-sdk/629608a18d3232dabe0b81a7ddd1a13d902e989f/images/favicon.ico -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extism/dotnet-sdk/629608a18d3232dabe0b81a7ddd1a13d902e989f/images/logo.png -------------------------------------------------------------------------------- /index.md: -------------------------------------------------------------------------------- 1 | --- 2 | _layout: landing 3 | --- 4 | 5 | [!INCLUDE [README](README.md)] 6 | 7 | ## API Docs 8 | Please see our [API docs](xref:Extism.Sdk) for detailed information on each type. -------------------------------------------------------------------------------- /nuget/Extism.runtime.win.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0;netstandard2.1 5 | true 6 | false 7 | 8 | 9 | 10 | Extism.runtime.win-x64 11 | 0.7.0 12 | Extism Contributors 13 | Internal implementation package for Extism to work on Windows x64 14 | extism, wasm, plugin 15 | BSD-3-Clause 16 | 17 | 18 | 19 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /nuget/runtimes/expected.txt: -------------------------------------------------------------------------------- 1 | win-x64.dll -------------------------------------------------------------------------------- /samples/Extism.Sdk.FSharpSample/Extism.Sdk.FSharpSample.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net9.0 6 | True 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /samples/Extism.Sdk.FSharpSample/Program.fs: -------------------------------------------------------------------------------- 1 | open Extism.Sdk 2 | open System 3 | open System.Text 4 | open System.Collections.Generic 5 | 6 | printfn "hiiii" 7 | 8 | let manifest = Manifest(new UrlWasmSource(Uri("https://github.com/extism/plugins/releases/latest/download/count_vowels_kvstore.wasm"))) 9 | 10 | let kvStore = new Dictionary() 11 | 12 | let functions = 13 | [| 14 | HostFunction.FromMethod("kv_read", IntPtr.Zero, fun (plugin: CurrentPlugin) (keyOffset: int64) -> 15 | let key = plugin.ReadString(keyOffset) 16 | let value = 17 | match kvStore.TryGetValue(key) with 18 | | true, v -> v 19 | | _ -> [| 0uy; 0uy; 0uy; 0uy |] // Default value if key not found 20 | 21 | Console.WriteLine($"Read {BitConverter.ToUInt32(value, 0)} from key={key}") 22 | plugin.WriteBytes(value) 23 | ) 24 | 25 | HostFunction.FromMethod("kv_write", IntPtr.Zero, fun (plugin: CurrentPlugin) (keyOffset: int64) (valueOffset: int64) -> 26 | let key = plugin.ReadString(keyOffset) 27 | let value = plugin.ReadBytes(valueOffset).ToArray() 28 | 29 | Console.WriteLine($"Writing value={BitConverter.ToUInt32(value, 0)} from key={key}") 30 | kvStore.[key] <- value 31 | ) 32 | |] 33 | 34 | let plugin = 35 | new Plugin(manifest, functions, withWasi = true) 36 | 37 | printfn "plugin created" 38 | 39 | let inputBytes = Encoding.UTF8.GetBytes("Hello, World!") 40 | let output = Encoding.UTF8.GetString(plugin.Call("count_vowels", inputBytes)) 41 | 42 | printfn "%s" output 43 | 44 | let output1 = Encoding.UTF8.GetString(plugin.Call("count_vowels", inputBytes)) 45 | // => {"count": 3, "total": 6, "vowels": "aeiouAEIOU"} 46 | 47 | let output2 = Encoding.UTF8.GetString(plugin.Call("count_vowels", inputBytes)) 48 | // => {"count": 3, "total": 9, "vowels": "aeiouAEIOU"} 49 | 50 | printfn "%s" output2 -------------------------------------------------------------------------------- /samples/Extism.Sdk.Sample/Extism.Sdk.Sample.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net9.0 6 | enable 7 | enable 8 | true 9 | True 10 | true 11 | 12 | 13 | 14 | 15 | PreserveNewest 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /samples/Extism.Sdk.Sample/Program.cs: -------------------------------------------------------------------------------- 1 | using Extism.Sdk; 2 | 3 | using System.Runtime.InteropServices; 4 | using System.Text; 5 | 6 | var kvStore = new Dictionary(); 7 | 8 | Console.WriteLine($"Version: {Plugin.ExtismVersion()}"); 9 | 10 | var userData = Marshal.StringToHGlobalAnsi("Hello again!"); 11 | 12 | var manifest = new Manifest(new UrlWasmSource("https://github.com/extism/plugins/releases/latest/download/count_vowels_kvstore.wasm")); 13 | 14 | var functions = new[] 15 | { 16 | HostFunction.FromMethod("kv_read", IntPtr.Zero, (CurrentPlugin plugin, long keyOffset) => 17 | { 18 | var key = plugin.ReadString(keyOffset); 19 | if (!kvStore.TryGetValue(key, out var value)) 20 | { 21 | value = new byte[] { 0, 0, 0, 0 }; 22 | } 23 | 24 | Console.WriteLine($"Read {BitConverter.ToUInt32(value)} from key={key}"); 25 | return plugin.WriteBytes(value); 26 | }), 27 | 28 | HostFunction.FromMethod("kv_write", IntPtr.Zero, (CurrentPlugin plugin, long keyOffset, long valueOffset) => 29 | { 30 | var key = plugin.ReadString(keyOffset); 31 | var value = plugin.ReadBytes(valueOffset); 32 | 33 | Console.WriteLine($"Writing value={BitConverter.ToUInt32(value)} from key={key}"); 34 | kvStore[key] = value.ToArray(); 35 | }) 36 | }; 37 | 38 | using var plugin = new Plugin(manifest, functions, withWasi: true); 39 | 40 | var output = Encoding.UTF8.GetString( 41 | plugin.Call("count_vowels", Encoding.UTF8.GetBytes("Hello World!")) 42 | ); 43 | 44 | Console.WriteLine($"Output: {output}"); 45 | 46 | output = Encoding.UTF8.GetString( 47 | plugin.Call("count_vowels", Encoding.UTF8.GetBytes("Hello World!")) 48 | ); 49 | 50 | Console.WriteLine($"Output: {output}"); -------------------------------------------------------------------------------- /samples/Extism.Sdk.Sample/README.md: -------------------------------------------------------------------------------- 1 | ## Example 1 2 | 3 | This example shows how you can use the library in the most basic way. 4 | It loads up the sample wasm plugin and lets you to pass inputs to it and show the ouput. -------------------------------------------------------------------------------- /src/Directory.build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | true 7 | 8 | 9 | true 10 | 11 | 12 | embedded 13 | 14 | true 15 | 16 | 17 | 18 | true 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Extism.Sdk/CurrentPlugin.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using System.Text; 3 | 4 | using Extism.Sdk.Native; 5 | 6 | namespace Extism.Sdk; 7 | 8 | /// 9 | /// Represents the current plugin. Can only be used within s. 10 | /// 11 | public unsafe class CurrentPlugin 12 | { 13 | private readonly nint _userData; 14 | internal CurrentPlugin(LibExtism.ExtismCurrentPlugin* nativeHandle, nint userData) 15 | { 16 | NativeHandle = nativeHandle; 17 | 18 | 19 | _userData = userData; 20 | } 21 | 22 | internal LibExtism.ExtismCurrentPlugin* NativeHandle { get; } 23 | 24 | /// 25 | /// Returns the user data object that was passed in when a was registered. 26 | /// 27 | [Obsolete("Use GetUserData instead.")] 28 | public nint UserData => _userData; 29 | 30 | /// 31 | /// Returns the user data object that was passed in when a was registered. 32 | /// 33 | /// 34 | /// 35 | public T? GetUserData() 36 | { 37 | if (_userData == IntPtr.Zero) 38 | { 39 | return default; 40 | } 41 | 42 | var handle1 = GCHandle.FromIntPtr(_userData); 43 | return (T?)handle1.Target; 44 | } 45 | 46 | /// 47 | /// Get the current plugin call's associated host context data. Returns null if call was made without host context. 48 | /// 49 | /// 50 | /// 51 | public T? GetCallHostContext() 52 | { 53 | var ptr = LibExtism.extism_current_plugin_host_context(NativeHandle); 54 | if (ptr == null) 55 | { 56 | return default; 57 | } 58 | 59 | var handle = GCHandle.FromIntPtr(new IntPtr(ptr)); 60 | return (T?)handle.Target; 61 | } 62 | 63 | /// 64 | /// Returns a offset to the memory of the currently running plugin. 65 | /// NOTE: this should only be called from host functions. 66 | /// 67 | /// 68 | public long GetMemory() 69 | { 70 | return LibExtism.extism_current_plugin_memory(NativeHandle); 71 | } 72 | 73 | /// 74 | /// Reads a string from a memory block using UTF8. 75 | /// 76 | /// 77 | /// 78 | public string ReadString(long offset) 79 | { 80 | return ReadString(offset, Encoding.UTF8); 81 | } 82 | 83 | /// 84 | /// Reads a string form a memory block. 85 | /// 86 | /// 87 | /// 88 | /// 89 | public string ReadString(long offset, Encoding encoding) 90 | { 91 | var buffer = ReadBytes(offset); 92 | 93 | return encoding.GetString(buffer); 94 | } 95 | 96 | /// 97 | /// Returns a span of bytes for a given block. 98 | /// 99 | /// 100 | /// 101 | public unsafe Span ReadBytes(long offset) 102 | { 103 | var mem = GetMemory(); 104 | var length = (int)BlockLength(offset); 105 | var ptr = (byte*)mem + offset; 106 | 107 | return new Span(ptr, length); 108 | } 109 | 110 | /// 111 | /// Writes a string into the current plugin memory using UTF-8 encoding and returns the offset of the block. 112 | /// 113 | /// 114 | public long WriteString(string value) 115 | => WriteString(value, Encoding.UTF8); 116 | 117 | /// 118 | /// Writes a string into the current plugin memory and returns the offset of the block. 119 | /// 120 | /// 121 | /// 122 | public long WriteString(string value, Encoding encoding) 123 | { 124 | var bytes = encoding.GetBytes(value); 125 | var offset = AllocateBlock(bytes.Length); 126 | WriteBytes(offset, bytes); 127 | 128 | return offset; 129 | } 130 | 131 | /// 132 | /// Writes a byte array into a newly allocated block of memory. 133 | /// 134 | /// 135 | /// Returns the offset of the allocated block 136 | public long WriteBytes(Span bytes) 137 | { 138 | var offset = AllocateBlock(bytes.Length); 139 | WriteBytes(offset, bytes); 140 | return offset; 141 | } 142 | 143 | /// 144 | /// Writes a byte array into a block of memory. 145 | /// 146 | /// 147 | /// 148 | public unsafe void WriteBytes(long offset, Span bytes) 149 | { 150 | var length = BlockLength(offset); 151 | if (length < bytes.Length) 152 | { 153 | throw new InvalidOperationException("Destination block length is less than source block length."); 154 | } 155 | 156 | var mem = GetMemory(); 157 | var ptr = (void*)(mem + offset); 158 | var destination = new Span(ptr, bytes.Length); 159 | 160 | bytes.CopyTo(destination); 161 | } 162 | 163 | /// 164 | /// Frees a block of memory belonging to the current plugin. 165 | /// 166 | /// 167 | public void FreeBlock(long offset) 168 | { 169 | LibExtism.extism_current_plugin_memory_free(NativeHandle, offset); 170 | } 171 | 172 | /// 173 | /// Allocate a memory block in the currently running plugin. 174 | /// 175 | /// 176 | /// 177 | /// 178 | public long AllocateBlock(long length) 179 | { 180 | return LibExtism.extism_current_plugin_memory_alloc(NativeHandle, length); 181 | } 182 | 183 | /// 184 | /// Get the length of an allocated block. 185 | /// NOTE: this should only be called from host functions. 186 | /// 187 | /// 188 | /// 189 | public long BlockLength(long offset) 190 | { 191 | return LibExtism.extism_current_plugin_memory_length(NativeHandle, offset); 192 | } 193 | } -------------------------------------------------------------------------------- /src/Extism.Sdk/Extism.Sdk.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.1;net8.0;net9.0 5 | enable 6 | enable 7 | True 8 | 11 9 | v 10 | True 11 | true 12 | 13 | 14 | 15 | Extism.Sdk 16 | Extism Contributors 17 | Extism SDK that allows hosting Extism plugins in .NET apps. 18 | extism, wasm, plugin 19 | BSD-3-Clause 20 | README.md 21 | 22 | 23 | 24 | true 25 | true 26 | snupkg 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | all 45 | runtime; build; native; contentfiles; analyzers; buildtransitive 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/Extism.Sdk/ExtismException.cs: -------------------------------------------------------------------------------- 1 | namespace Extism.Sdk; 2 | 3 | using System; 4 | 5 | /// 6 | /// Represents errors that occur during calling Extism functions. 7 | /// 8 | public class ExtismException : Exception 9 | { 10 | /// 11 | /// Initializes a new instance of the class. 12 | /// 13 | public ExtismException() 14 | { 15 | } 16 | 17 | /// 18 | /// Initializes a new instance of the class with a specified error message. 19 | /// 20 | /// The message that describes the error . 21 | public ExtismException(string message) 22 | : base(message) 23 | { 24 | } 25 | 26 | /// 27 | /// Initializes a new instance of the class 28 | /// with a specified error message and a reference to the inner exception 29 | /// that is the cause of this exception. 30 | /// 31 | /// The message that describes the error. 32 | /// 33 | /// The exception that is the cause of the current exception, or a null reference 34 | /// (Nothing in Visual Basic) if no inner exception is specified. 35 | /// 36 | public ExtismException(string message, Exception innerException) 37 | : base(message, innerException) 38 | { 39 | } 40 | } -------------------------------------------------------------------------------- /src/Extism.Sdk/HostFunction.cs: -------------------------------------------------------------------------------- 1 | using Extism.Sdk.Native; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Runtime.InteropServices; 4 | 5 | namespace Extism.Sdk; 6 | 7 | /// 8 | /// A host function signature. 9 | /// 10 | /// Plugin Index 11 | /// Input parameters 12 | /// Output parameters, the host function can change this. 13 | public delegate void ExtismFunction(CurrentPlugin plugin, Span inputs, Span outputs); 14 | 15 | /// 16 | /// A function provided by the host that plugins can call. 17 | /// 18 | public class HostFunction : IDisposable 19 | { 20 | private const int DisposedMarker = 1; 21 | private int _disposed; 22 | private readonly ExtismFunction _function; 23 | private readonly LibExtism.InternalExtismFunction _callback; 24 | private readonly GCHandle? _userDataHandle; 25 | 26 | /// 27 | /// Registers a Host Function. 28 | /// 29 | /// The literal name of the function, how it would be called from a . 30 | /// The types of the input arguments/parameters the caller will provide. 31 | /// The types of the output returned from the host function to the . 32 | /// 33 | /// A state object that will be preserved and can be retrieved during function execution using . 34 | /// This allows you to maintain context between function calls. 35 | /// 36 | unsafe public HostFunction( 37 | string functionName, 38 | Span inputTypes, 39 | Span outputTypes, 40 | object? userData, 41 | ExtismFunction hostFunction) 42 | { 43 | // Make sure we store the delegate reference in a field so that it doesn't get garbage collected 44 | _function = hostFunction; 45 | _callback = CallbackImpl; 46 | _userDataHandle = userData is null ? null : GCHandle.Alloc(userData); 47 | 48 | fixed (ExtismValType* inputs = inputTypes) 49 | fixed (ExtismValType* outputs = outputTypes) 50 | { 51 | NativeHandle = LibExtism.extism_function_new( 52 | functionName, 53 | inputs, 54 | inputTypes.Length, 55 | outputs, 56 | outputTypes.Length, 57 | _callback, 58 | _userDataHandle is null ? IntPtr.Zero : GCHandle.ToIntPtr(_userDataHandle.Value), 59 | IntPtr.Zero); 60 | } 61 | } 62 | 63 | internal nint NativeHandle { get; } 64 | 65 | /// 66 | /// Sets the function namespace. By default it's set to `env`. 67 | /// 68 | /// 69 | public void SetNamespace(string ns) 70 | { 71 | if (!string.IsNullOrEmpty(ns)) 72 | { 73 | LibExtism.extism_function_set_namespace(NativeHandle, ns); 74 | } 75 | } 76 | 77 | /// 78 | /// Sets the function namespace. By default it's set to `extism:host/user`. 79 | /// 80 | /// 81 | /// 82 | public HostFunction WithNamespace(string ns) 83 | { 84 | this.SetNamespace(ns); 85 | return this; 86 | } 87 | 88 | private unsafe void CallbackImpl( 89 | LibExtism.ExtismCurrentPlugin* plugin, 90 | ExtismVal* inputsPtr, 91 | uint n_inputs, 92 | ExtismVal* outputsPtr, 93 | uint n_outputs, 94 | nint data) 95 | { 96 | var outputs = new Span(outputsPtr, (int)n_outputs); 97 | var inputs = new Span(inputsPtr, (int)n_inputs); 98 | 99 | _function(new CurrentPlugin(plugin, data), inputs, outputs); 100 | } 101 | 102 | /// 103 | /// Registers a from a method that takes no parameters an returns no values. 104 | /// 105 | /// The literal name of the function, how it would be called from a . 106 | /// 107 | /// A state object that will be preserved and can be retrieved during function execution using . 108 | /// This allows you to maintain context between function calls. 109 | /// The host function implementation. 110 | /// 111 | public static HostFunction FromMethod( 112 | string functionName, 113 | object userData, 114 | Action callback) 115 | { 116 | var inputTypes = new ExtismValType[] { }; 117 | var returnType = new ExtismValType[] { }; 118 | 119 | return new HostFunction(functionName, inputTypes, returnType, userData, 120 | (CurrentPlugin plugin, Span inputs, Span outputs) => 121 | { 122 | callback(plugin); 123 | }); 124 | } 125 | 126 | /// 127 | /// Registers a from a method that takes 1 parameter an returns no values. Supported parameter types: 128 | /// , , , , , 129 | /// 130 | /// Type of first parameter. Supported parameter types: , , , , , 131 | /// The literal name of the function, how it would be called from a . 132 | /// 133 | /// A state object that will be preserved and can be retrieved during function execution using . 134 | /// This allows you to maintain context between function calls. 135 | /// The host function implementation. 136 | /// 137 | public static HostFunction FromMethod( 138 | string functionName, 139 | object userData, 140 | Action callback) 141 | where I1 : struct 142 | { 143 | var inputTypes = new ExtismValType[] { ToExtismType() }; 144 | var returnType = new ExtismValType[] { }; 145 | 146 | return new HostFunction(functionName, inputTypes, returnType, userData, 147 | (CurrentPlugin plugin, Span inputs, Span outputs) => 148 | { 149 | callback(plugin, GetValue(inputs[0])); 150 | }); 151 | } 152 | 153 | /// 154 | /// Registers a from a method that takes 2 parameters an returns no values. Supported parameter types: 155 | /// , , , , , 156 | /// 157 | /// Type of the first parameter. Supported parameter types: , , , , , 158 | /// Type of the second parameter. Supported parameter types: , , , , , 159 | /// The literal name of the function, how it would be called from a . 160 | /// 161 | /// A state object that will be preserved and can be retrieved during function execution using . 162 | /// This allows you to maintain context between function calls. 163 | /// The host function implementation. 164 | /// 165 | public static HostFunction FromMethod( 166 | string functionName, 167 | object userData, 168 | Action callback) 169 | where I1 : struct 170 | where I2 : struct 171 | { 172 | var inputTypes = new ExtismValType[] { ToExtismType(), ToExtismType() }; 173 | 174 | var returnType = new ExtismValType[] { }; 175 | 176 | return new HostFunction(functionName, inputTypes, returnType, userData, 177 | (CurrentPlugin plugin, Span inputs, Span outputs) => 178 | { 179 | callback(plugin, GetValue(inputs[0]), GetValue(inputs[1])); 180 | }); 181 | } 182 | 183 | /// 184 | /// Registers a from a method that takes 3 parameters an returns no values. Supported parameter types: 185 | /// , , , , , 186 | /// 187 | /// Type of the first parameter. Supported parameter types: , , , , , 188 | /// Type of the second parameter. Supported parameter types: , , , , , 189 | /// Type of the third parameter. Supported parameter types: , , , , , 190 | /// The literal name of the function, how it would be called from a . 191 | /// 192 | /// A state object that will be preserved and can be retrieved during function execution using . 193 | /// This allows you to maintain context between function calls. 194 | /// The host function implementation. 195 | /// 196 | public static HostFunction FromMethod( 197 | string functionName, 198 | object userData, 199 | Action callback) 200 | where I1 : struct 201 | where I2 : struct 202 | where I3 : struct 203 | { 204 | var inputTypes = new ExtismValType[] { ToExtismType(), ToExtismType(), ToExtismType() }; 205 | var returnType = new ExtismValType[] { }; 206 | 207 | return new HostFunction(functionName, inputTypes, returnType, userData, 208 | (CurrentPlugin plugin, Span inputs, Span outputs) => 209 | { 210 | callback(plugin, GetValue(inputs[0]), GetValue(inputs[1]), GetValue(inputs[2])); 211 | }); 212 | } 213 | 214 | /// 215 | /// Registers a from a method that takes no parameters an returns a value. Supported return types: 216 | /// , , , , , 217 | /// 218 | /// Type of the first parameter. Supported parameter types: , , , , , 219 | /// The literal name of the function, how it would be called from a . 220 | /// 221 | /// A state object that will be preserved and can be retrieved during function execution using . 222 | /// This allows you to maintain context between function calls. 223 | /// The host function implementation. 224 | /// 225 | public static HostFunction FromMethod( 226 | string functionName, 227 | object userData, 228 | Func callback) 229 | where R : struct 230 | { 231 | var inputTypes = new ExtismValType[] { }; 232 | var returnType = new ExtismValType[] { ToExtismType() }; 233 | 234 | return new HostFunction(functionName, inputTypes, returnType, userData, 235 | (CurrentPlugin plugin, Span inputs, Span outputs) => 236 | { 237 | var value = callback(plugin); 238 | SetValue(ref outputs[0], value); 239 | }); 240 | } 241 | 242 | /// 243 | /// Registers a from a method that takes 1 parameter an returns a value. Supported return and parameter types: 244 | /// , , , , , 245 | /// 246 | /// Type of the first parameter. Supported parameter types: , , , , , 247 | /// Type of the first parameter. Supported parameter types: , , , , , 248 | /// The literal name of the function, how it would be called from a . 249 | /// 250 | /// A state object that will be preserved and can be retrieved during function execution using . 251 | /// This allows you to maintain context between function calls. 252 | /// The host function implementation. 253 | /// 254 | public static HostFunction FromMethod( 255 | string functionName, 256 | object userData, 257 | Func callback) 258 | where I1 : struct 259 | where R : struct 260 | { 261 | var inputTypes = new ExtismValType[] { ToExtismType() }; 262 | var returnType = new ExtismValType[] { ToExtismType() }; 263 | 264 | return new HostFunction(functionName, inputTypes, returnType, userData, 265 | (CurrentPlugin plugin, Span inputs, Span outputs) => 266 | { 267 | var value = callback(plugin, GetValue(inputs[0])); 268 | SetValue(ref outputs[0], value); 269 | }); 270 | } 271 | 272 | /// 273 | /// Registers a from a method that takes 2 parameter an returns a value. Supported return and parameter types: 274 | /// , , , , , 275 | /// 276 | /// Type of the first parameter. Supported parameter types: , , , , , 277 | /// Type of the second parameter. Supported parameter types: , , , , , 278 | /// Type of the first parameter. Supported parameter types: , , , , , 279 | /// The literal name of the function, how it would be called from a . 280 | /// 281 | /// A state object that will be preserved and can be retrieved during function execution using . 282 | /// This allows you to maintain context between function calls. 283 | /// The host function implementation. 284 | /// 285 | public static HostFunction FromMethod( 286 | string functionName, 287 | object userData, 288 | Func callback) 289 | where I1 : struct 290 | where I2 : struct 291 | where R : struct 292 | { 293 | var inputTypes = new ExtismValType[] { ToExtismType(), ToExtismType() }; 294 | var returnType = new ExtismValType[] { ToExtismType() }; 295 | 296 | return new HostFunction(functionName, inputTypes, returnType, userData, 297 | (CurrentPlugin plugin, Span inputs, Span outputs) => 298 | { 299 | var value = callback(plugin, GetValue(inputs[0]), GetValue(inputs[1])); 300 | SetValue(ref outputs[0], value); 301 | }); 302 | } 303 | 304 | /// 305 | /// Registers a from a method that takes 3 parameter an returns a value. Supported return and parameter types: 306 | /// , , , , , 307 | /// 308 | /// Type of the first parameter. Supported parameter types: , , , , , 309 | /// Type of the second parameter. Supported parameter types: , , , , , 310 | /// Type of the third parameter. Supported parameter types: , , , , , 311 | /// Type of the first parameter. Supported parameter types: , , , , , 312 | /// The literal name of the function, how it would be called from a . 313 | /// 314 | /// A state object that will be preserved and can be retrieved during function execution using . 315 | /// This allows you to maintain context between function calls. 316 | /// The host function implementation. 317 | /// 318 | public static HostFunction FromMethod( 319 | string functionName, 320 | object userData, 321 | Func callback) 322 | where I1 : struct 323 | where I2 : struct 324 | where I3 : struct 325 | where R : struct 326 | { 327 | var inputTypes = new ExtismValType[] { ToExtismType(), ToExtismType(), ToExtismType() }; 328 | var returnType = new ExtismValType[] { ToExtismType() }; 329 | 330 | return new HostFunction(functionName, inputTypes, returnType, userData, 331 | (CurrentPlugin plugin, Span inputs, Span outputs) => 332 | { 333 | var value = callback(plugin, GetValue(inputs[0]), GetValue(inputs[1]), GetValue(inputs[2])); 334 | SetValue(ref outputs[0], value); 335 | }); 336 | } 337 | 338 | private static ExtismValType ToExtismType() where T : struct 339 | { 340 | return typeof(T) switch 341 | { 342 | Type t when t == typeof(int) || t == typeof(uint) => ExtismValType.I32, 343 | Type t when t == typeof(long) || t == typeof(ulong) => ExtismValType.I64, 344 | Type t when t == typeof(float) => ExtismValType.F32, 345 | Type t when t == typeof(double) => ExtismValType.F64, 346 | _ => throw new NotImplementedException($"Unsupported type: {typeof(T).Name}"), 347 | }; 348 | } 349 | 350 | private static T GetValue(ExtismVal val) where T : struct 351 | { 352 | return typeof(T) switch 353 | { 354 | Type intType when intType == typeof(int) && val.t == ExtismValType.I32 => (T)(object)val.v.i32, 355 | Type longType when longType == typeof(long) && val.t == ExtismValType.I64 => (T)(object)val.v.i64, 356 | Type floatType when floatType == typeof(float) && val.t == ExtismValType.F32 => (T)(object)val.v.f32, 357 | Type doubleType when doubleType == typeof(double) && val.t == ExtismValType.F64 => (T)(object)val.v.f64, 358 | _ => throw new InvalidOperationException($"Unsupported conversion from {Enum.GetName(typeof(ExtismValType), val.t)} to {typeof(T).Name}") 359 | }; 360 | } 361 | 362 | private static void SetValue(ref ExtismVal val, T t) 363 | { 364 | if (t is int i32) 365 | { 366 | val.t = ExtismValType.I32; 367 | val.v.i32 = i32; 368 | } 369 | else if (t is uint u32) 370 | { 371 | val.t = ExtismValType.I32; 372 | val.v.i32 = (int)u32; 373 | } 374 | else if (t is long i64) 375 | { 376 | val.t = ExtismValType.I64; 377 | val.v.i64 = i64; 378 | } 379 | else if (t is ulong u64) 380 | { 381 | val.t = ExtismValType.I64; 382 | val.v.i64 = (long)u64; 383 | } 384 | else if (t is float f32) 385 | { 386 | val.t = ExtismValType.F32; 387 | val.v.f32 = f32; 388 | } 389 | else if (t is double f64) 390 | { 391 | val.t = ExtismValType.F64; 392 | val.v.f64 = f64; 393 | } 394 | else 395 | { 396 | throw new InvalidOperationException($"Unsupported value type: {typeof(T).Name}"); 397 | } 398 | } 399 | 400 | /// 401 | /// Frees all resources held by this Host Function. 402 | /// 403 | public void Dispose() 404 | { 405 | if (Interlocked.Exchange(ref _disposed, DisposedMarker) == DisposedMarker) 406 | { 407 | // Already disposed. 408 | return; 409 | } 410 | 411 | Dispose(true); 412 | GC.SuppressFinalize(this); 413 | } 414 | 415 | /// 416 | /// Throw an appropriate exception if the Host Function has been disposed. 417 | /// 418 | /// 419 | protected void CheckNotDisposed() 420 | { 421 | Interlocked.MemoryBarrier(); 422 | if (_disposed == DisposedMarker) 423 | { 424 | ThrowDisposedException(); 425 | } 426 | } 427 | 428 | [DoesNotReturn] 429 | private static void ThrowDisposedException() 430 | { 431 | throw new ObjectDisposedException(nameof(HostFunction)); 432 | } 433 | 434 | /// 435 | /// Frees all resources held by this Host Function. 436 | /// 437 | unsafe protected virtual void Dispose(bool disposing) 438 | { 439 | if (disposing) 440 | { 441 | _userDataHandle?.Free(); 442 | } 443 | 444 | // Free up unmanaged resources 445 | LibExtism.extism_function_free(NativeHandle); 446 | } 447 | 448 | /// 449 | /// Destructs the current Host Function and frees all resources used by it. 450 | /// 451 | ~HostFunction() 452 | { 453 | Dispose(false); 454 | } 455 | } -------------------------------------------------------------------------------- /src/Extism.Sdk/LibExtism.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace Extism.Sdk.Native; 4 | 5 | /// 6 | /// A union type for host function argument/return values. 7 | /// 8 | [StructLayout(LayoutKind.Explicit)] 9 | public struct ExtismValUnion 10 | { 11 | /// 12 | /// Set this for 32 bit integers 13 | /// 14 | [FieldOffset(0)] 15 | public int i32; 16 | 17 | /// 18 | /// Set this for 64 bit integers 19 | /// 20 | [FieldOffset(0)] 21 | public long i64; 22 | 23 | /// 24 | /// Set this for 64 bit integers 25 | /// 26 | [FieldOffset(0)] 27 | public long ptr; 28 | 29 | /// 30 | /// Set this for 32 bit floats 31 | /// 32 | [FieldOffset(0)] 33 | public float f32; 34 | 35 | /// 36 | /// Set this for 64 bit floats 37 | /// 38 | [FieldOffset(0)] 39 | public double f64; 40 | } 41 | 42 | /// 43 | /// Represents Wasm data types that Extism can understand 44 | /// 45 | public enum ExtismValType : int 46 | { 47 | /// 48 | /// Signed 32 bit integer. Equivalent of or 49 | /// 50 | I32, 51 | 52 | /// 53 | /// Signed 64 bit integer. Equivalent of or 54 | /// 55 | I64, 56 | 57 | /// 58 | /// A wrapper around to specify arguments that are pointers to memory blocks 59 | /// 60 | PTR = I64, 61 | 62 | /// 63 | /// Floating point 32 bit integer. Equivalent of 64 | /// 65 | F32, 66 | 67 | /// 68 | /// Floating point 64 bit integer. Equivalent of 69 | /// 70 | F64, 71 | 72 | /// 73 | /// A 128 bit number. 74 | /// 75 | V128, 76 | 77 | /// 78 | /// A reference to opaque data in the Wasm instance. 79 | /// 80 | FuncRef, 81 | 82 | /// 83 | /// A reference to opaque data in the Wasm instance. 84 | /// 85 | ExternRef 86 | } 87 | 88 | /// 89 | /// `ExtismVal` holds the type and value of a function argument/return 90 | /// 91 | [StructLayout(LayoutKind.Sequential)] 92 | public struct ExtismVal 93 | { 94 | /// 95 | /// The type for the argument 96 | /// 97 | public ExtismValType t; 98 | 99 | /// 100 | /// The value for the argument 101 | /// 102 | public ExtismValUnion v; 103 | } 104 | 105 | /// 106 | /// Functions exposed by the native Extism library. 107 | /// 108 | internal static class LibExtism 109 | { 110 | /// 111 | /// An Extism Plugin 112 | /// 113 | [StructLayout(LayoutKind.Sequential)] 114 | internal struct ExtismPlugin { } 115 | 116 | [StructLayout(LayoutKind.Sequential)] 117 | internal struct ExtismCompiledPlugin { } 118 | 119 | [StructLayout(LayoutKind.Sequential)] 120 | internal struct ExtismCurrentPlugin { } 121 | 122 | /// 123 | /// Host function signature 124 | /// 125 | /// 126 | /// 127 | /// 128 | /// 129 | /// 130 | /// 131 | unsafe internal delegate void InternalExtismFunction(ExtismCurrentPlugin* plugin, ExtismVal* inputs, uint n_inputs, ExtismVal* outputs, uint n_outputs, IntPtr data); 132 | 133 | /// 134 | /// Returns a pointer to the memory of the currently running plugin. 135 | /// NOTE: this should only be called from host functions. 136 | /// 137 | /// 138 | /// 139 | [DllImport("extism", EntryPoint = "extism_current_plugin_memory")] 140 | unsafe internal static extern long extism_current_plugin_memory(ExtismCurrentPlugin* plugin); 141 | 142 | /// 143 | /// Allocate a memory block in the currently running plugin 144 | /// 145 | /// 146 | /// 147 | /// 148 | [DllImport("extism", EntryPoint = "extism_current_plugin_memory_alloc")] 149 | unsafe internal static extern long extism_current_plugin_memory_alloc(ExtismCurrentPlugin* plugin, long n); 150 | 151 | /// 152 | /// Get the length of an allocated block. 153 | /// NOTE: this should only be called from host functions. 154 | /// 155 | /// 156 | /// 157 | /// 158 | [DllImport("extism", EntryPoint = "extism_current_plugin_memory_length")] 159 | unsafe internal static extern long extism_current_plugin_memory_length(ExtismCurrentPlugin* plugin, long n); 160 | 161 | /// 162 | /// Get the length of an allocated block. 163 | /// NOTE: this should only be called from host functions. 164 | /// 165 | /// 166 | /// 167 | [DllImport("extism", EntryPoint = "extism_current_plugin_memory_free")] 168 | unsafe internal static extern void extism_current_plugin_memory_free(ExtismCurrentPlugin* plugin, long ptr); 169 | 170 | /// 171 | /// Create a new host function. 172 | /// 173 | /// function name, this should be valid UTF-8 174 | /// argument types 175 | /// number of argument types 176 | /// return types 177 | /// number of return types 178 | /// the function to call 179 | /// a pointer that will be passed to the function when it's called this value should live as long as the function exists 180 | /// a callback to release the `user_data` value when the resulting `ExtismFunction` is freed. 181 | /// 182 | [DllImport("extism", EntryPoint = "extism_function_new")] 183 | unsafe internal static extern IntPtr extism_function_new(string name, ExtismValType* inputs, long nInputs, ExtismValType* outputs, long nOutputs, InternalExtismFunction func, IntPtr userData, IntPtr freeUserData); 184 | 185 | /// 186 | /// Set the namespace of an 187 | /// 188 | /// 189 | /// 190 | [DllImport("extism", EntryPoint = "extism_function_set_namespace")] 191 | internal static extern void extism_function_set_namespace(IntPtr ptr, string @namespace); 192 | 193 | /// 194 | /// Free an 195 | /// 196 | /// 197 | [DllImport("extism", EntryPoint = "extism_function_free")] 198 | internal static extern void extism_function_free(IntPtr ptr); 199 | 200 | /// 201 | /// Load a WASM plugin. 202 | /// 203 | /// A WASM module (wat or wasm) or a JSON encoded manifest. 204 | /// The length of the `wasm` parameter. 205 | /// Array of host function pointers. 206 | /// Number of host functions. 207 | /// Enables/disables WASI. 208 | /// 209 | /// 210 | [DllImport("extism")] 211 | unsafe internal static extern ExtismPlugin* extism_plugin_new(byte* wasm, long wasmSize, IntPtr* functions, long nFunctions, [MarshalAs(UnmanagedType.I1)] bool withWasi, out char** errmsg); 212 | 213 | /// 214 | /// Load a WASM plugin with fuel limit. 215 | /// 216 | /// A WASM module (wat or wasm) or a JSON encoded manifest. 217 | /// The length of the `wasm` parameter. 218 | /// Array of host function pointers. 219 | /// Number of host functions. 220 | /// Enables/disables WASI. 221 | /// Max number of instructions that can be executed by the plugin. 222 | /// 223 | /// 224 | [DllImport("extism")] 225 | unsafe internal static extern ExtismPlugin* extism_plugin_new_with_fuel_limit(byte* wasm, long wasmSize, IntPtr* functions, long nFunctions, [MarshalAs(UnmanagedType.I1)] bool withWasi, long fuelLimit, out char** errmsg); 226 | 227 | /// 228 | /// Frees a plugin error message. 229 | /// 230 | /// 231 | [DllImport("extism")] 232 | unsafe internal static extern void extism_plugin_new_error_free(IntPtr errorMessage); 233 | 234 | /// 235 | /// Remove a plugin from the registry and free associated memory. 236 | /// 237 | /// Pointer to the plugin you want to free. 238 | [DllImport("extism")] 239 | unsafe internal static extern void extism_plugin_free(ExtismPlugin* plugin); 240 | /// 241 | /// Pre-compile an Extism plugin 242 | /// 243 | /// A WASM module (wat or wasm) or a JSON encoded manifest. 244 | /// The length of the `wasm` parameter. 245 | /// Array of host function pointers. 246 | /// Number of host functions. 247 | /// Enables/disables WASI. 248 | /// 249 | /// 250 | [DllImport("extism")] 251 | unsafe internal static extern ExtismCompiledPlugin* extism_compiled_plugin_new(byte* wasm, long wasmSize, IntPtr* functions, long nFunctions, [MarshalAs(UnmanagedType.I1)] bool withWasi, out char** errmsg); 252 | 253 | /// 254 | /// Free `ExtismCompiledPlugin` 255 | /// 256 | /// 257 | [DllImport("extism")] 258 | unsafe internal static extern void extism_compiled_plugin_free(ExtismCompiledPlugin* plugin); 259 | 260 | /// 261 | /// Create a new plugin from an `ExtismCompiledPlugin` 262 | /// 263 | /// 264 | [DllImport("extism")] 265 | unsafe internal static extern ExtismPlugin* extism_plugin_new_from_compiled(ExtismCompiledPlugin* compiled, out char** errmsg); 266 | 267 | /// 268 | /// Enable HTTP response headers in plugins using `extism:host/env::http_request` 269 | /// 270 | /// 271 | /// 272 | [DllImport("extism")] 273 | unsafe internal static extern ExtismPlugin* extism_plugin_allow_http_response_headers(ExtismPlugin* plugin); 274 | 275 | /// 276 | /// Get handle for plugin cancellation 277 | /// 278 | /// 279 | /// 280 | [DllImport("extism")] 281 | internal unsafe static extern IntPtr extism_plugin_cancel_handle(ExtismPlugin* plugin); 282 | 283 | /// 284 | /// Cancel a running plugin 285 | /// 286 | /// 287 | /// 288 | [DllImport("extism")] 289 | internal static extern bool extism_plugin_cancel(IntPtr handle); 290 | 291 | /// 292 | /// Update plugin config values, this will merge with the existing values. 293 | /// 294 | /// Pointer to the plugin you want to update the configurations for. 295 | /// The configuration JSON encoded in UTF8. 296 | /// The length of the `json` parameter. 297 | /// 298 | [DllImport("extism")] 299 | unsafe internal static extern bool extism_plugin_config(ExtismPlugin* plugin, byte* json, int jsonLength); 300 | 301 | /// 302 | /// Returns true if funcName exists. 303 | /// 304 | /// 305 | /// 306 | /// 307 | [DllImport("extism")] 308 | unsafe internal static extern bool extism_plugin_function_exists(ExtismPlugin* plugin, string funcName); 309 | 310 | /// 311 | /// Call a function. 312 | /// 313 | /// 314 | /// The function to call. 315 | /// Input data. 316 | /// The length of the `data` parameter. 317 | /// 318 | [DllImport("extism")] 319 | unsafe internal static extern int extism_plugin_call(ExtismPlugin* plugin, string funcName, byte* data, int dataLen); 320 | 321 | /// 322 | /// Call a function with host context. 323 | /// 324 | /// 325 | /// The function to call. 326 | /// Input data. 327 | /// The length of the `data` parameter. 328 | /// a pointer to context data that will be available in host functions 329 | /// 330 | [DllImport("extism")] 331 | unsafe internal static extern int extism_plugin_call_with_host_context(ExtismPlugin* plugin, string funcName, byte* data, long dataLen, IntPtr hostContext); 332 | 333 | /// 334 | /// Get the current plugin's associated host context data. Returns null if call was made without host context. 335 | /// 336 | /// 337 | /// 338 | [DllImport("extism")] 339 | unsafe internal static extern void* extism_current_plugin_host_context(ExtismCurrentPlugin* plugin); 340 | 341 | /// 342 | /// Get the error associated with a Plugin 343 | /// 344 | /// A plugin pointer 345 | /// 346 | [DllImport("extism")] 347 | unsafe internal static extern IntPtr extism_plugin_error(ExtismPlugin* plugin); 348 | 349 | /// 350 | /// Get the length of a plugin's output data. 351 | /// 352 | /// 353 | /// 354 | [DllImport("extism")] 355 | unsafe internal static extern long extism_plugin_output_length(ExtismPlugin* plugin); 356 | 357 | /// 358 | /// Get the plugin's output data. 359 | /// 360 | /// 361 | /// 362 | [DllImport("extism")] 363 | unsafe internal static extern IntPtr extism_plugin_output_data(ExtismPlugin* plugin); 364 | 365 | /// 366 | /// Reset the Extism runtime, this will invalidate all allocated memory 367 | /// 368 | /// 369 | /// 370 | [DllImport("extism")] 371 | unsafe internal static extern bool extism_plugin_reset(ExtismPlugin* plugin); 372 | 373 | /// 374 | /// Get a plugin's ID, the returned bytes are a 16 byte buffer that represent a UUIDv4 375 | /// 376 | /// 377 | /// 378 | [DllImport("extism")] 379 | unsafe internal static extern byte* extism_plugin_id(ExtismPlugin* plugin); 380 | 381 | /// 382 | /// Set log file and level for file logger. 383 | /// 384 | /// 385 | /// 386 | /// 387 | [DllImport("extism")] 388 | internal static extern bool extism_log_file(string filename, string logLevel); 389 | 390 | /// 391 | /// Enable a custom log handler, this will buffer logs until `extism_log_drain` is called. 392 | /// this will buffer logs until `extism_log_drain` is called 393 | /// 394 | /// 395 | /// 396 | [DllImport("extism")] 397 | internal static extern bool extism_log_custom(string logLevel); 398 | 399 | internal delegate void LoggingSink(string line, ulong length); 400 | 401 | /// 402 | /// Calls the provided callback function for each buffered log line. 403 | /// This is only needed when `extism_log_custom` is used. 404 | /// 405 | /// 406 | [DllImport("extism")] 407 | internal static extern void extism_log_drain(LoggingSink callback); 408 | 409 | /// 410 | /// Get Extism Runtime version. 411 | /// 412 | /// 413 | [DllImport("extism")] 414 | internal static extern IntPtr extism_version(); 415 | } 416 | -------------------------------------------------------------------------------- /src/Extism.Sdk/LogLevel.cs: -------------------------------------------------------------------------------- 1 | namespace Extism.Sdk; 2 | 3 | /// 4 | /// Extism Log Levels 5 | /// 6 | public enum LogLevel 7 | { 8 | /// 9 | /// Designates very serious errors. 10 | /// 11 | Error = 1, 12 | 13 | /// 14 | /// Designates hazardous situations. 15 | /// 16 | Warn, 17 | 18 | /// 19 | /// Designates useful information. 20 | /// 21 | Info, 22 | 23 | /// 24 | /// Designates lower priority information. 25 | /// 26 | Debug, 27 | 28 | /// 29 | /// Designates very low priority, often extremely verbose, information. 30 | /// 31 | Trace 32 | } 33 | -------------------------------------------------------------------------------- /src/Extism.Sdk/Manifest.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using System.Text.Json; 3 | 4 | namespace Extism.Sdk 5 | { 6 | [JsonSerializable(typeof(Manifest))] 7 | [JsonSerializable(typeof(HttpMethod))] 8 | [JsonSerializable(typeof(Dictionary))] 9 | [JsonSerializable(typeof(WasmSource))] 10 | [JsonSerializable(typeof(ByteArrayWasmSource))] 11 | [JsonSerializable(typeof(PathWasmSource))] 12 | [JsonSerializable(typeof(UrlWasmSource))] 13 | internal partial class ManifestJsonContext : JsonSerializerContext 14 | { 15 | 16 | } 17 | 18 | /// 19 | /// The manifest is a description of your plugin and some of the runtime constraints to apply to it. 20 | /// You can think of it as a blueprint to build your plugin. 21 | /// 22 | public class Manifest 23 | { 24 | /// 25 | /// Create an empty manifest. 26 | /// 27 | public Manifest() 28 | { 29 | 30 | } 31 | 32 | /// 33 | /// Create a manifest from one or more Wasm sources. 34 | /// 35 | /// 36 | public Manifest(params WasmSource[] sources) 37 | { 38 | foreach (var source in sources) 39 | { 40 | Sources.Add(source); 41 | } 42 | } 43 | 44 | /// 45 | /// List of Wasm sources. See and . 46 | /// 47 | [JsonPropertyName("wasm")] 48 | public IList Sources { get; set; } = new List(); 49 | 50 | /// 51 | /// Configures memory for the Wasm runtime. 52 | /// Memory is described in units of pages (64KB) and represent contiguous chunks of addressable memory. 53 | /// 54 | [JsonPropertyName("memory")] 55 | public MemoryOptions? MemoryOptions { get; set; } 56 | 57 | /// 58 | /// List of host names the plugins can access. Example: 59 | /// 60 | /// AllowedHosts = new List<string> { 61 | /// "www.example.com", 62 | /// "api.*.com", 63 | /// "example.*", 64 | /// } 65 | /// 66 | /// 67 | [JsonPropertyName("allowed_hosts")] 68 | public IList AllowedHosts { get; set; } = new List(); 69 | 70 | /// 71 | /// List of directories that can be accessed by the plugins. Examples: 72 | /// 73 | /// AllowedPaths = new Dictionary<string, string> 74 | /// { 75 | /// { "/usr/plugins/1/data", "/data" }, // src, dest 76 | /// { "d:/plugins/1/data", "/data" } // src, dest 77 | /// }; 78 | /// 79 | /// 80 | [JsonPropertyName("allowed_paths")] 81 | public IDictionary AllowedPaths { get; set; } = new Dictionary(); 82 | 83 | /// 84 | /// Configurations available to the plugins. Examples: 85 | /// 86 | /// Config = new Dictionary<string, string> 87 | /// { 88 | /// { "userId", "55" }, // key, value 89 | /// { "mySecret", "super-secret-key" } // key, value 90 | /// }; 91 | /// 92 | /// 93 | [JsonPropertyName("config")] 94 | public IDictionary Config { get; set; } = new Dictionary(); 95 | 96 | /// 97 | /// Plugin call timeout. 98 | /// 99 | [JsonPropertyName("timeout_ms")] 100 | [JsonConverter(typeof(TimeSpanMillisecondsConverter))] 101 | public TimeSpan? Timeout { get; set; } 102 | } 103 | 104 | /// 105 | /// Configures memory for the Wasm runtime. 106 | /// Memory is described in units of pages (64KB) and represent contiguous chunks of addressable memory. 107 | /// 108 | public class MemoryOptions 109 | { 110 | /// 111 | /// Max number of pages. Each page is 64KB. 112 | /// 113 | [JsonPropertyName("max_pages")] 114 | public int MaxPages { get; set; } 115 | 116 | 117 | /// 118 | /// Max number of bytes allowed in an HTTP response when using extism_http_request. 119 | /// 120 | [JsonPropertyName("max_http_response_bytes")] 121 | public int MaxHttpResponseBytes { get; set; } 122 | 123 | 124 | /// 125 | /// Max number of bytes allowed in the Extism var store 126 | /// 127 | [JsonPropertyName("max_var_bytes")] 128 | public int MaxVarBytes { get; set; } 129 | } 130 | 131 | /// 132 | /// A named Wasm source. 133 | /// 134 | public abstract class WasmSource 135 | { 136 | /// 137 | /// Logical name of the Wasm source 138 | /// 139 | [JsonPropertyName("name")] 140 | public string? Name { get; set; } 141 | 142 | /// 143 | /// Hash of the WASM source 144 | /// 145 | [JsonPropertyName("hash")] 146 | public string? Hash { get; set; } 147 | } 148 | 149 | /// 150 | /// Wasm Source represented by a file referenced by a path. 151 | /// 152 | public class PathWasmSource : WasmSource 153 | { 154 | /// 155 | /// Constructor 156 | /// 157 | /// path to wasm plugin. 158 | /// 159 | /// 160 | public PathWasmSource(string path, string? name = null, string? hash = null) 161 | { 162 | Path = System.IO.Path.GetFullPath(path); 163 | Name = name ?? System.IO.Path.GetFileNameWithoutExtension(path); 164 | Hash = hash; 165 | } 166 | 167 | /// 168 | /// Path to wasm plugin. 169 | /// 170 | [JsonPropertyName("path")] 171 | public string Path { get; set; } 172 | } 173 | 174 | /// 175 | /// Wasm Source represented by a file referenced by a path. 176 | /// 177 | public class UrlWasmSource : WasmSource 178 | { 179 | /// 180 | /// Constructor 181 | /// 182 | /// uri to wasm plugin. 183 | /// 184 | /// 185 | public UrlWasmSource(string url, string? name = null, string? hash = null) : this(new Uri(url), name, hash) 186 | { 187 | 188 | } 189 | 190 | /// 191 | /// Constructor 192 | /// 193 | /// uri to wasm plugin. 194 | /// 195 | /// 196 | public UrlWasmSource(Uri url, string? name = null, string? hash = null) 197 | { 198 | Url = url; 199 | Name = name; 200 | Hash = hash; 201 | } 202 | 203 | /// 204 | /// Uri to wasm plugin. 205 | /// 206 | [JsonPropertyName("url")] 207 | public Uri Url { get; set; } 208 | 209 | /// 210 | /// HTTP headers 211 | /// 212 | [JsonPropertyName("headers")] 213 | public Dictionary Headers { get; set; } = new(); 214 | 215 | /// 216 | /// HTTP Method 217 | /// 218 | [JsonPropertyName("method")] 219 | public HttpMethod? Method { get; set; } 220 | } 221 | 222 | /// 223 | /// HTTP defines a set of request methods to indicate the desired action to be performed for a given resource. 224 | /// 225 | public enum HttpMethod 226 | { 227 | /// 228 | /// The GET method requests a representation of the specified resource. Requests using GET should only retrieve data. 229 | /// 230 | GET, 231 | 232 | /// 233 | /// The HEAD method asks for a response identical to a GET request, but without the response body. 234 | /// 235 | HEAD, 236 | 237 | /// 238 | /// The POST method submits an entity to the specified resource, often causing a change in state or side effects on the server. 239 | /// 240 | POST, 241 | 242 | /// 243 | /// The PUT method replaces all current representations of the target resource with the request payload. 244 | /// 245 | PUT, 246 | 247 | /// 248 | /// The DELETE method deletes the specified resource. 249 | /// 250 | DELETE, 251 | 252 | /// 253 | /// The CONNECT method establishes a tunnel to the server identified by the target resource. 254 | /// 255 | CONNECT, 256 | 257 | /// 258 | /// The OPTIONS method describes the communication options for the target resource. 259 | /// 260 | OPTIONS, 261 | 262 | /// 263 | /// The TRACE method performs a message loop-back test along the path to the target resource. 264 | /// 265 | TRACE, 266 | 267 | /// 268 | /// The PATCH method applies partial modifications to a resource. 269 | /// 270 | PATCH, 271 | } 272 | 273 | /// 274 | /// Wasm Source represented by raw bytes. 275 | /// 276 | public class ByteArrayWasmSource : WasmSource 277 | { 278 | /// 279 | /// Constructor 280 | /// 281 | /// the byte array representing the Wasm code 282 | /// 283 | /// 284 | public ByteArrayWasmSource(byte[] data, string? name, string? hash = null) 285 | { 286 | Data = data; 287 | Name = name; 288 | Hash = hash; 289 | } 290 | 291 | /// 292 | /// The byte array representing the Wasm code 293 | /// 294 | [JsonPropertyName("data")] 295 | public byte[] Data { get; } 296 | } 297 | 298 | class WasmSourceConverter : JsonConverter 299 | { 300 | public override WasmSource Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 301 | { 302 | throw new NotImplementedException(); 303 | } 304 | 305 | public override void Write(Utf8JsonWriter writer, WasmSource value, JsonSerializerOptions options) 306 | { 307 | // Clone it because a JsonSerializerOptions can't be shared by multiple JsonSerializerContexts 308 | var context = new ManifestJsonContext(new JsonSerializerOptions(options)); 309 | if (value is PathWasmSource path) 310 | { 311 | JsonSerializer.Serialize(writer, path, context.PathWasmSource); 312 | } 313 | else if (value is ByteArrayWasmSource bytes) 314 | { 315 | JsonSerializer.Serialize(writer, bytes, context.ByteArrayWasmSource); 316 | } 317 | else if (value is UrlWasmSource uri) 318 | { 319 | JsonSerializer.Serialize(writer, uri, context.UrlWasmSource); 320 | } 321 | else 322 | { 323 | throw new ArgumentOutOfRangeException(nameof(value), "Unknown Wasm Source"); 324 | } 325 | } 326 | } 327 | 328 | class TimeSpanMillisecondsConverter : JsonConverter 329 | { 330 | public override TimeSpan? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 331 | { 332 | if (reader.TokenType == JsonTokenType.Null) 333 | { 334 | return null; 335 | } 336 | else if (reader.TokenType == JsonTokenType.Number) 337 | { 338 | long milliseconds = reader.GetInt64(); 339 | return TimeSpan.FromMilliseconds(milliseconds); 340 | } 341 | 342 | throw new JsonException($"Expected number, but got {reader.TokenType}"); 343 | } 344 | 345 | public override void Write(Utf8JsonWriter writer, TimeSpan? value, JsonSerializerOptions options) 346 | { 347 | if (value is null) 348 | { 349 | writer.WriteNullValue(); 350 | } 351 | else 352 | { 353 | writer.WriteNumberValue((long)value.Value.TotalMilliseconds); 354 | } 355 | } 356 | } 357 | } 358 | -------------------------------------------------------------------------------- /src/Extism.Sdk/Plugin.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Runtime.InteropServices; 3 | using System.Text; 4 | using System.Text.Json; 5 | using System.Text.Json.Serialization; 6 | using System.Text.Json.Serialization.Metadata; 7 | 8 | using Extism.Sdk.Native; 9 | 10 | namespace Extism.Sdk; 11 | 12 | /// 13 | /// Represents a WASM Extism plugin. 14 | /// 15 | public unsafe class Plugin : IDisposable 16 | { 17 | private const int DisposedMarker = 1; 18 | 19 | private static readonly JsonSerializerOptions? _serializerOptions = new() 20 | { 21 | PropertyNameCaseInsensitive = true, 22 | }; 23 | 24 | private readonly HostFunction[] _functions; 25 | private int _disposed; 26 | private readonly IntPtr _cancelHandle; 27 | 28 | /// 29 | /// Native pointer to the Extism Plugin. 30 | /// 31 | internal LibExtism.ExtismPlugin* NativeHandle { get; } 32 | 33 | /// 34 | /// Instantiate a plugin from a compiled plugin. 35 | /// 36 | /// 37 | internal Plugin(CompiledPlugin plugin) 38 | { 39 | char** errorMsgPtr; 40 | 41 | var handle = LibExtism.extism_plugin_new_from_compiled(plugin.NativeHandle, out errorMsgPtr); 42 | if (handle == null) 43 | { 44 | var msg = "Unable to intialize a plugin from compiled plugin"; 45 | 46 | if (errorMsgPtr is not null) 47 | { 48 | msg = Marshal.PtrToStringAnsi(new IntPtr(errorMsgPtr)); 49 | } 50 | 51 | throw new ExtismException(msg ?? "Unknown error"); 52 | } 53 | 54 | NativeHandle = handle; 55 | _functions = plugin.Functions; 56 | _cancelHandle = LibExtism.extism_plugin_cancel_handle(NativeHandle); 57 | } 58 | 59 | /// 60 | /// Initialize a plugin from a Manifest. 61 | /// 62 | /// 63 | /// 64 | /// 65 | public Plugin(Manifest manifest, HostFunction[] functions, PluginIntializationOptions options) 66 | { 67 | _functions = functions; 68 | 69 | var jsonOptions = new JsonSerializerOptions 70 | { 71 | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, 72 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase, 73 | }; 74 | 75 | jsonOptions.Converters.Add(new WasmSourceConverter()); 76 | jsonOptions.Converters.Add(new JsonStringEnumConverter()); 77 | 78 | var jsonContext = new ManifestJsonContext(jsonOptions); 79 | var json = JsonSerializer.Serialize(manifest, jsonContext.Manifest); 80 | 81 | var bytes = Encoding.UTF8.GetBytes(json); 82 | 83 | var functionHandles = functions.Select(f => f.NativeHandle).ToArray(); 84 | fixed (byte* wasmPtr = bytes) 85 | fixed (IntPtr* functionsPtr = functionHandles) 86 | { 87 | NativeHandle = Initialize(wasmPtr, bytes.Length, functions, functionsPtr, options); 88 | } 89 | 90 | _cancelHandle = LibExtism.extism_plugin_cancel_handle(NativeHandle); 91 | } 92 | 93 | /// 94 | /// Create a plugin from a Manifest. 95 | /// 96 | /// 97 | /// 98 | /// 99 | public Plugin(Manifest manifest, HostFunction[] functions, bool withWasi) : this(manifest, functions, new PluginIntializationOptions { WithWasi = withWasi }) 100 | { 101 | 102 | } 103 | 104 | /// 105 | /// Create and load a plugin from a byte array. 106 | /// 107 | /// A WASM module (wat or wasm) or a JSON encoded manifest. 108 | /// List of host functions expected by the plugin. 109 | /// Enable/Disable WASI. 110 | public Plugin(ReadOnlySpan wasm, HostFunction[] functions, bool withWasi) 111 | { 112 | _functions = functions; 113 | 114 | var functionHandles = functions.Select(f => f.NativeHandle).ToArray(); 115 | fixed (byte* wasmPtr = wasm) 116 | fixed (IntPtr* functionsPtr = functionHandles) 117 | { 118 | NativeHandle = Initialize(wasmPtr, wasm.Length, functions, functionsPtr, new PluginIntializationOptions { WithWasi = withWasi }); 119 | } 120 | 121 | _cancelHandle = LibExtism.extism_plugin_cancel_handle(NativeHandle); 122 | } 123 | 124 | private unsafe LibExtism.ExtismPlugin* Initialize(byte* wasmPtr, int wasmLength, HostFunction[] functions, IntPtr* functionsPtr, PluginIntializationOptions options) 125 | { 126 | char** errorMsgPtr; 127 | 128 | var handle = options.FuelLimit is null ? 129 | LibExtism.extism_plugin_new(wasmPtr, wasmLength, functionsPtr, functions.Length, options.WithWasi, out errorMsgPtr) : 130 | LibExtism.extism_plugin_new_with_fuel_limit(wasmPtr, wasmLength, functionsPtr, functions.Length, options.WithWasi, options.FuelLimit.Value, out errorMsgPtr); 131 | 132 | if (handle == null) 133 | { 134 | var msg = "Unable to create plugin"; 135 | 136 | if (errorMsgPtr is not null) 137 | { 138 | msg = Marshal.PtrToStringAnsi(new IntPtr(errorMsgPtr)); 139 | } 140 | 141 | throw new ExtismException(msg ?? "Unknown error"); 142 | } 143 | 144 | return handle; 145 | } 146 | 147 | /// 148 | /// Get the plugin's ID. 149 | /// 150 | public Guid Id 151 | { 152 | get 153 | { 154 | var bytes = new Span(LibExtism.extism_plugin_id(NativeHandle), 16); 155 | return new Guid(bytes); 156 | } 157 | } 158 | 159 | /// 160 | /// Reset the Extism runtime, this will invalidate all allocated memory 161 | /// 162 | /// 163 | public bool Reset() 164 | { 165 | CheckNotDisposed(); 166 | return LibExtism.extism_plugin_reset(NativeHandle); 167 | } 168 | 169 | /// 170 | /// Enable HTTP response headers in plugins using `extism:host/env::http_request` 171 | /// 172 | public void AllowHttpResponseHeaders() 173 | { 174 | LibExtism.extism_plugin_allow_http_response_headers(NativeHandle); 175 | } 176 | 177 | /// 178 | /// Update plugin config values, this will merge with the existing values. 179 | /// 180 | /// 181 | /// 182 | /// 183 | public bool UpdateConfig(Dictionary value, JsonSerializerOptions serializerOptions) 184 | { 185 | var jsonContext = new ManifestJsonContext(serializerOptions); 186 | 187 | var json = JsonSerializer.Serialize(value, jsonContext.DictionaryStringString); 188 | var bytes = Encoding.UTF8.GetBytes(json); 189 | return UpdateConfig(bytes); 190 | } 191 | 192 | /// 193 | /// Update plugin config values, this will merge with the existing values. 194 | /// 195 | /// The configuration JSON encoded in UTF8. 196 | unsafe public bool UpdateConfig(ReadOnlySpan json) 197 | { 198 | CheckNotDisposed(); 199 | 200 | fixed (byte* jsonPtr = json) 201 | { 202 | return LibExtism.extism_plugin_config(NativeHandle, jsonPtr, json.Length); 203 | } 204 | } 205 | 206 | /// 207 | /// Checks if a specific function exists in the current plugin. 208 | /// 209 | unsafe public bool FunctionExists(string name) 210 | { 211 | CheckNotDisposed(); 212 | 213 | return LibExtism.extism_plugin_function_exists(NativeHandle, name); 214 | } 215 | 216 | /// 217 | /// Calls a function in the current plugin and returns the output as a byte buffer. 218 | /// 219 | /// Name of the function in the plugin to invoke. 220 | /// A buffer to provide as input to the function. 221 | /// CancellationToken used for cancelling the Extism call. 222 | /// The output of the function call 223 | /// 224 | unsafe public ReadOnlySpan Call(string functionName, ReadOnlySpan input, CancellationToken? cancellationToken = null) 225 | { 226 | return CallImpl(functionName, input, hostContext: null, cancellationToken); 227 | } 228 | 229 | /// 230 | /// Calls a function in the current plugin and returns the output as a byte buffer. 231 | /// 232 | /// 233 | /// Name of the function in the plugin to invoke. 234 | /// A buffer to provide as input to the function. 235 | /// An object that will be passed back to HostFunctions 236 | /// CancellationToken used for cancelling the Extism call. 237 | /// The output of the function call 238 | /// 239 | unsafe public ReadOnlySpan CallWithHostContext(string functionName, ReadOnlySpan input, T hostContext, CancellationToken? cancellationToken = null) 240 | { 241 | GCHandle handle = GCHandle.Alloc(hostContext); 242 | try 243 | { 244 | return CallImpl(functionName, input, GCHandle.ToIntPtr(handle), cancellationToken); 245 | } 246 | finally 247 | { 248 | handle.Free(); 249 | } 250 | } 251 | 252 | private ReadOnlySpan CallImpl(string functionName, ReadOnlySpan input, IntPtr? hostContext, CancellationToken? cancellationToken = null) 253 | { 254 | CheckNotDisposed(); 255 | cancellationToken?.ThrowIfCancellationRequested(); 256 | using var _ = cancellationToken?.Register(() => LibExtism.extism_plugin_cancel(_cancelHandle)); 257 | 258 | fixed (byte* dataPtr = input) 259 | { 260 | int response = hostContext.HasValue ? 261 | LibExtism.extism_plugin_call_with_host_context(NativeHandle, functionName, dataPtr, input.Length, hostContext.Value) : 262 | LibExtism.extism_plugin_call(NativeHandle, functionName, dataPtr, input.Length); 263 | 264 | var errorMsg = GetError(); 265 | if (errorMsg != null) 266 | { 267 | throw new ExtismException($"{errorMsg}. Exit Code: {response}"); 268 | } 269 | return OutputData(); 270 | } 271 | } 272 | 273 | /// 274 | /// Calls a function in the current plugin and returns the output as a UTF8 encoded string. 275 | /// 276 | /// Name of the function in the plugin to invoke. 277 | /// A string that will be UTF8 encoded and passed to the plugin. 278 | /// CancellationToken used for cancelling the Extism call. 279 | /// The output of the function as a UTF8 encoded string 280 | public string Call(string functionName, string input, CancellationToken? cancellationToken = null) 281 | { 282 | var inputBytes = Encoding.UTF8.GetBytes(input); 283 | var outputBytes = Call(functionName, inputBytes, cancellationToken); 284 | return Encoding.UTF8.GetString(outputBytes); 285 | } 286 | 287 | /// 288 | /// Calls a function on the plugin with a payload. The payload is serialized into JSON and encoded in UTF8. 289 | /// 290 | /// Type of the input payload. 291 | /// Type of the output payload returned by the function. 292 | /// Name of the function in the plugin to invoke. 293 | /// An object that will be serialized into JSON and passed into the function as a UTF8 encoded string. 294 | /// JSON serialization options used for serialization/derserialization 295 | /// CancellationToken used for cancelling the Extism call. 296 | /// 297 | 298 | #if NET7_0_OR_GREATER 299 | [RequiresUnreferencedCode("This function call can break in AOT compiled apps because it uses reflection for serialization. Use an overload that accepts an JsonTypeInfo instead.")] 300 | [RequiresDynamicCode("This function call can break in AOT compiled apps because it uses reflection for serialization. Use an overload that accepts an JsonTypeInfo instead.")] 301 | #endif 302 | public TOutput? Call(string functionName, TInput input, JsonSerializerOptions? serializerOptions = null, CancellationToken? cancellationToken = null) 303 | { 304 | var inputJson = JsonSerializer.Serialize(input, serializerOptions ?? _serializerOptions); 305 | var outputJson = Call(functionName, inputJson, cancellationToken); 306 | return JsonSerializer.Deserialize(outputJson, serializerOptions ?? _serializerOptions); 307 | } 308 | 309 | /// 310 | /// Calls a function on the plugin with a payload. The payload is serialized into JSON and encoded in UTF8. 311 | /// 312 | /// Type of the input payload. 313 | /// Type of the output payload returned by the function. 314 | /// Name of the function in the plugin to invoke. 315 | /// An object that will be serialized into JSON and passed into the function as a UTF8 encoded string. 316 | /// Metadata about input type. 317 | /// Metadata about output type. 318 | /// CancellationToken used for cancelling the Extism call. 319 | /// 320 | public TOutput? Call(string functionName, TInput input, JsonTypeInfo inputJsonInfo, JsonTypeInfo outputJsonInfo, CancellationToken? cancellationToken = null) 321 | { 322 | var inputJson = JsonSerializer.Serialize(input, inputJsonInfo); 323 | var outputJson = Call(functionName, inputJson, cancellationToken); 324 | return JsonSerializer.Deserialize(outputJson, outputJsonInfo); 325 | } 326 | 327 | /// 328 | /// Calls a function on the plugin and deserializes the output as UTF8 encoded JSON. 329 | /// 330 | /// Type of the output payload returned by the function. 331 | /// Name of the function in the plugin to invoke. 332 | /// Function input. 333 | /// JSON serialization options used for serialization/derserialization. 334 | /// CancellationToken used for cancelling the Extism call. 335 | /// 336 | #if NET7_0_OR_GREATER 337 | [RequiresUnreferencedCode("This function call can break in AOT compiled apps because it uses reflection for serialization. Use an overload that accepts an JsonTypeInfo instead.")] 338 | [RequiresDynamicCode("This function call can break in AOT compiled apps because it uses reflection for serialization. Use an overload that accepts an JsonTypeInfo instead.")] 339 | #endif 340 | public TOutput? Call(string functionName, string input, JsonSerializerOptions? serializerOptions = null, CancellationToken? cancellationToken = null) 341 | { 342 | var outputJson = Call(functionName, input, cancellationToken); 343 | return JsonSerializer.Deserialize(outputJson, serializerOptions ?? _serializerOptions); 344 | } 345 | 346 | /// 347 | /// Calls a function on the plugin with a payload. The payload is serialized into JSON and encoded in UTF8. 348 | /// 349 | /// Type of the output payload returned by the function. 350 | /// Name of the function in the plugin to invoke. 351 | /// Function input. 352 | /// Metadata about output type. 353 | /// CancellationToken used for cancelling the Extism call. 354 | /// 355 | public TOutput? Call(string functionName, string input, JsonTypeInfo outputJsonInfo, CancellationToken? cancellationToken = null) 356 | { 357 | var outputJson = Call(functionName, input, cancellationToken); 358 | return JsonSerializer.Deserialize(outputJson, outputJsonInfo); 359 | } 360 | 361 | /// 362 | /// Get the length of a plugin's output data. 363 | /// 364 | /// 365 | unsafe internal int OutputLength() 366 | { 367 | CheckNotDisposed(); 368 | 369 | return (int)LibExtism.extism_plugin_output_length(NativeHandle); 370 | } 371 | 372 | /// 373 | /// Get the plugin's output data. 374 | /// 375 | internal ReadOnlySpan OutputData() 376 | { 377 | CheckNotDisposed(); 378 | 379 | var length = OutputLength(); 380 | 381 | unsafe 382 | { 383 | var ptr = LibExtism.extism_plugin_output_data(NativeHandle).ToPointer(); 384 | return new Span(ptr, length); 385 | } 386 | } 387 | 388 | /// 389 | /// Get the error associated with the current plugin. 390 | /// 391 | /// 392 | unsafe internal string? GetError() 393 | { 394 | CheckNotDisposed(); 395 | 396 | var result = LibExtism.extism_plugin_error(NativeHandle); 397 | return Marshal.PtrToStringUTF8(result); 398 | } 399 | 400 | /// 401 | /// Frees all resources held by this Plugin. 402 | /// 403 | public void Dispose() 404 | { 405 | if (Interlocked.Exchange(ref _disposed, DisposedMarker) == DisposedMarker) 406 | { 407 | // Already disposed. 408 | return; 409 | } 410 | 411 | Dispose(true); 412 | GC.SuppressFinalize(this); 413 | } 414 | 415 | /// 416 | /// Throw an appropriate exception if the plugin has been disposed. 417 | /// 418 | /// 419 | protected void CheckNotDisposed() 420 | { 421 | Interlocked.MemoryBarrier(); 422 | if (_disposed == DisposedMarker) 423 | { 424 | ThrowDisposedException(); 425 | } 426 | } 427 | 428 | [DoesNotReturn] 429 | private static void ThrowDisposedException() 430 | { 431 | throw new ObjectDisposedException(nameof(Plugin)); 432 | } 433 | 434 | /// 435 | /// Frees all resources held by this Plugin. 436 | /// 437 | unsafe protected virtual void Dispose(bool disposing) 438 | { 439 | if (disposing) 440 | { 441 | // Free up any managed resources here 442 | } 443 | 444 | // Free up unmanaged resources 445 | LibExtism.extism_plugin_free(NativeHandle); 446 | } 447 | 448 | /// 449 | /// Destructs the current Plugin and frees all resources used by it. 450 | /// 451 | ~Plugin() 452 | { 453 | Dispose(false); 454 | } 455 | 456 | /// 457 | /// Get Extism Runtime version. 458 | /// 459 | /// 460 | public static string ExtismVersion() 461 | { 462 | var version = LibExtism.extism_version(); 463 | return Marshal.PtrToStringAnsi(version) ?? "unknown"; 464 | } 465 | 466 | /// 467 | /// Set log file and level 468 | /// 469 | /// Log file path 470 | /// Minimum log level 471 | public static void ConfigureFileLogging(string path, LogLevel level) 472 | { 473 | var logLevel = Enum.GetName(typeof(LogLevel), level)?.ToLowerInvariant() 474 | ?? throw new ArgumentOutOfRangeException(nameof(level)); 475 | 476 | LibExtism.extism_log_file(path, logLevel); 477 | } 478 | 479 | /// 480 | /// Enable a custom log handler, this will buffer logs until is called. 481 | /// 482 | /// 483 | public static void ConfigureCustomLogging(LogLevel level) 484 | { 485 | var logLevel = Enum.GetName(typeof(LogLevel), level)?.ToLowerInvariant() 486 | ?? throw new ArgumentOutOfRangeException(nameof(level)); 487 | 488 | LibExtism.extism_log_custom(logLevel); 489 | } 490 | 491 | /// 492 | /// Calls the provided callback function for each buffered log line. 493 | /// This only needed when is used. 494 | /// 495 | /// 496 | public static void DrainCustomLogs(LoggingSink callback) 497 | { 498 | LibExtism.extism_log_drain((line, length) => 499 | { 500 | callback(line); 501 | }); 502 | } 503 | } 504 | 505 | /// 506 | /// Options for initializing a plugin. 507 | /// 508 | public class PluginIntializationOptions 509 | { 510 | /// 511 | /// Enable WASI support. 512 | /// 513 | public bool WithWasi { get; set; } 514 | 515 | /// 516 | /// Limits number of instructions that can be executed by the plugin. 517 | /// 518 | public long? FuelLimit { get; set; } 519 | } 520 | 521 | /// 522 | /// Custom logging callback. 523 | /// 524 | /// 525 | public delegate void LoggingSink(string line); 526 | 527 | /// 528 | /// A pre-compiled plugin ready to be instantiated. 529 | /// 530 | public unsafe class CompiledPlugin : IDisposable 531 | { 532 | private const int DisposedMarker = 1; 533 | private int _disposed; 534 | 535 | internal LibExtism.ExtismCompiledPlugin* NativeHandle { get; } 536 | internal HostFunction[] Functions { get; } 537 | 538 | /// 539 | /// Compile a plugin from a Manifest. 540 | /// 541 | /// 542 | /// 543 | /// 544 | public CompiledPlugin(Manifest manifest, HostFunction[] functions, bool withWasi) 545 | { 546 | Functions = functions; 547 | 548 | var options = new JsonSerializerOptions 549 | { 550 | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, 551 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase, 552 | }; 553 | 554 | options.Converters.Add(new WasmSourceConverter()); 555 | options.Converters.Add(new JsonStringEnumConverter()); 556 | 557 | var jsonContext = new ManifestJsonContext(options); 558 | var json = JsonSerializer.Serialize(manifest, jsonContext.Manifest); 559 | 560 | var bytes = Encoding.UTF8.GetBytes(json); 561 | 562 | var functionHandles = functions.Select(f => f.NativeHandle).ToArray(); 563 | fixed (byte* wasmPtr = bytes) 564 | fixed (IntPtr* functionsPtr = functionHandles) 565 | { 566 | NativeHandle = Initialize(wasmPtr, bytes.Length, functions, withWasi, functionsPtr); 567 | } 568 | } 569 | 570 | /// 571 | /// Instantiate a plugin from this compiled plugin. 572 | /// 573 | /// 574 | public Plugin Instantiate() 575 | { 576 | CheckNotDisposed(); 577 | return new Plugin(this); 578 | } 579 | 580 | private unsafe LibExtism.ExtismCompiledPlugin* Initialize(byte* wasmPtr, int wasmLength, HostFunction[] functions, bool withWasi, IntPtr* functionsPtr) 581 | { 582 | char** errorMsgPtr; 583 | 584 | var handle = LibExtism.extism_compiled_plugin_new(wasmPtr, wasmLength, functionsPtr, functions.Length, withWasi, out errorMsgPtr); 585 | if (handle == null) 586 | { 587 | var msg = "Unable to compile plugin"; 588 | 589 | if (errorMsgPtr is not null) 590 | { 591 | msg = Marshal.PtrToStringAnsi(new IntPtr(errorMsgPtr)); 592 | } 593 | 594 | throw new ExtismException(msg ?? "Unknown error"); 595 | } 596 | 597 | return handle; 598 | } 599 | 600 | 601 | /// 602 | /// Frees all resources held by this Plugin. 603 | /// 604 | public void Dispose() 605 | { 606 | if (Interlocked.Exchange(ref _disposed, DisposedMarker) == DisposedMarker) 607 | { 608 | // Already disposed. 609 | return; 610 | } 611 | 612 | Dispose(true); 613 | GC.SuppressFinalize(this); 614 | } 615 | 616 | /// 617 | /// Throw an appropriate exception if the plugin has been disposed. 618 | /// 619 | /// 620 | protected void CheckNotDisposed() 621 | { 622 | Interlocked.MemoryBarrier(); 623 | if (_disposed == DisposedMarker) 624 | { 625 | ThrowDisposedException(); 626 | } 627 | } 628 | 629 | [DoesNotReturn] 630 | private static void ThrowDisposedException() 631 | { 632 | throw new ObjectDisposedException(nameof(Plugin)); 633 | } 634 | 635 | /// 636 | /// Frees all resources held by this Plugin. 637 | /// 638 | unsafe protected virtual void Dispose(bool disposing) 639 | { 640 | if (disposing) 641 | { 642 | // Free up any managed resources here 643 | } 644 | 645 | // Free up unmanaged resources 646 | LibExtism.extism_compiled_plugin_free(NativeHandle); 647 | } 648 | 649 | /// 650 | /// Destructs the current Plugin and frees all resources used by it. 651 | /// 652 | ~CompiledPlugin() 653 | { 654 | Dispose(false); 655 | } 656 | } -------------------------------------------------------------------------------- /src/Extism.Sdk/README.md: -------------------------------------------------------------------------------- 1 | ## Extism.Sdk 2 | Extism SDK that allows hosting Extism plugins in .NET apps. -------------------------------------------------------------------------------- /test/Extism.Sdk.Benchmarks/Extism.Sdk.Benchmarks.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net9.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | PreserveNewest 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /test/Extism.Sdk.Benchmarks/Program.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Running; 2 | using BenchmarkDotNet.Attributes; 3 | 4 | using Extism.Sdk; 5 | 6 | using System.Reflection; 7 | 8 | var summary = BenchmarkRunner.Run(); 9 | 10 | public class CompiledPluginBenchmarks 11 | { 12 | private const int N = 1000; 13 | private const string _input = "Hello, World!"; 14 | private const string _function = "count_vowels"; 15 | private readonly Manifest _manifest; 16 | 17 | public CompiledPluginBenchmarks() 18 | { 19 | var binDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!; 20 | _manifest = new Manifest(new PathWasmSource(Path.Combine(binDirectory, "wasm", "code.wasm"), "main")); 21 | } 22 | 23 | [Benchmark] 24 | public void CompiledPluginInstantiate() 25 | { 26 | using var compiledPlugin = new CompiledPlugin(_manifest, [], withWasi: true); 27 | 28 | for (var i = 0; i < N; i++) 29 | { 30 | using var plugin = compiledPlugin.Instantiate(); 31 | var response = plugin.Call(_function, _input); 32 | } 33 | } 34 | 35 | [Benchmark] 36 | public void PluginInstantiate() 37 | { 38 | for (var i = 0; i < N; i++) 39 | { 40 | using var plugin = new Plugin(_manifest, [], withWasi: true); 41 | var response = plugin.Call(_function, _input); 42 | } 43 | } 44 | } 45 | 46 | public class CountVowelsResponse 47 | { 48 | public int Count { get; set; } 49 | public int Total { get; set; } 50 | public string? Vowels { get; set; } 51 | } -------------------------------------------------------------------------------- /test/Extism.Sdk/BasicTests.cs: -------------------------------------------------------------------------------- 1 | using Extism.Sdk.Native; 2 | 3 | using Shouldly; 4 | 5 | using System.Runtime.InteropServices; 6 | using System.Text; 7 | 8 | using Xunit; 9 | 10 | namespace Extism.Sdk.Tests; 11 | 12 | public class BasicTests 13 | { 14 | [Fact] 15 | public void Alloc() 16 | { 17 | using var plugin = Helpers.LoadPlugin("alloc.wasm"); 18 | _ = plugin.Call("run_test", Array.Empty()); 19 | } 20 | 21 | [Fact] 22 | public void GetId() 23 | { 24 | using var plugin = Helpers.LoadPlugin("alloc.wasm"); 25 | var id = plugin.Id; 26 | Assert.NotEqual(Guid.Empty, id); 27 | } 28 | 29 | [Fact] 30 | public void Fail() 31 | { 32 | using var plugin = Helpers.LoadPlugin("fail.wasm"); 33 | 34 | Should.Throw(() => plugin.Call("run_test", Array.Empty())); 35 | } 36 | 37 | 38 | [Theory] 39 | [InlineData("abc", 1)] 40 | [InlineData("", 2)] 41 | public void Exit(string code, int expected) 42 | { 43 | using var plugin = Helpers.LoadPlugin("exit.wasm", m => 44 | { 45 | m.Config["code"] = code; 46 | }); 47 | 48 | var exception = Should.Throw(() => plugin.Call("_start", Array.Empty())); 49 | 50 | exception.Message.ShouldContain(expected.ToString()); 51 | exception.Message.ShouldContain("error while executing at wasm backtrace"); 52 | } 53 | 54 | [Fact] 55 | public void Timeout() 56 | { 57 | using var plugin = Helpers.LoadPlugin("sleep.wasm", m => 58 | { 59 | m.Timeout = TimeSpan.FromMilliseconds(50); 60 | m.Config["duration"] = "3"; // sleep for 3 seconds 61 | }); 62 | 63 | Should.Throw(() => plugin.Call("run_test", Array.Empty())) 64 | .Message.ShouldContain("timeout"); 65 | } 66 | 67 | [Fact] 68 | public void Cancel() 69 | { 70 | using var plugin = Helpers.LoadPlugin("sleep.wasm", m => 71 | { 72 | m.Config["duration"] = "1"; // sleep for 1 seconds 73 | }); 74 | 75 | for (var i = 0; i < 3; i++) 76 | { 77 | var cts = new CancellationTokenSource(); 78 | cts.CancelAfter(TimeSpan.FromMilliseconds(50)); 79 | 80 | Should.Throw(() => plugin.Call("run_test", Array.Empty(), cts.Token)) 81 | .Message.ShouldContain("timeout"); 82 | 83 | Should.Throw(() => plugin.Call("run_test", Array.Empty(), cts.Token)); 84 | } 85 | 86 | // We should be able to call the plugin normally after a cancellation 87 | plugin.Call("run_test", Array.Empty()); 88 | } 89 | 90 | [Fact] 91 | public void FileSystem() 92 | { 93 | using var plugin = Helpers.LoadPlugin("fs.wasm", m => 94 | { 95 | m.AllowedPaths.Add("data", "/mnt"); 96 | }); 97 | 98 | var output = plugin.Call("_start", Array.Empty()); 99 | var text = Encoding.UTF8.GetString(output); 100 | 101 | text.ShouldBe("hello world!"); 102 | } 103 | 104 | [Theory] 105 | [InlineData("code.wasm", "count_vowels", true)] 106 | [InlineData("code.wasm", "i_dont_exist", false)] 107 | public void FunctionExists(string fileName, string functionName, bool expected) 108 | { 109 | using var plugin = Helpers.LoadPlugin(fileName); 110 | 111 | var actual = plugin.FunctionExists(functionName); 112 | actual.ShouldBe(expected); 113 | } 114 | 115 | [Fact] 116 | public void CountHelloWorldVowels() 117 | { 118 | using var plugin = Helpers.LoadPlugin("code.wasm"); 119 | 120 | var response = plugin.Call("count_vowels", "Hello World"); 121 | response.ShouldContain("\"count\":3"); 122 | } 123 | 124 | [Fact] 125 | public void CountVowelsJson() 126 | { 127 | using var plugin = Helpers.LoadPlugin("code.wasm"); 128 | 129 | var response = plugin.Call("count_vowels", "Hello World"); 130 | 131 | response.ShouldNotBeNull(); 132 | response.Count.ShouldBe(3); 133 | } 134 | 135 | [Fact] 136 | public void CountVowelsHostFunctionsBackCompat() 137 | { 138 | for (int i = 0; i < 100; i++) 139 | { 140 | var userData = Marshal.StringToHGlobalAnsi("Hello again!"); 141 | 142 | using var helloWorld = new HostFunction( 143 | "hello_world", 144 | new[] { ExtismValType.PTR }, 145 | new[] { ExtismValType.PTR }, 146 | userData, 147 | HelloWorld); 148 | 149 | using var plugin = Helpers.LoadPlugin("code-functions.wasm", config: null, helloWorld); 150 | 151 | var response = plugin.Call("count_vowels", Encoding.UTF8.GetBytes("Hello World")); 152 | Encoding.UTF8.GetString(response).ShouldBe("{\"count\": 3}"); 153 | } 154 | 155 | void HelloWorld(CurrentPlugin plugin, Span inputs, Span outputs) 156 | { 157 | Console.WriteLine("Hello from .NET!"); 158 | 159 | 160 | #pragma warning disable CS0618 // Type or member is obsolete 161 | var text = Marshal.PtrToStringAnsi(plugin.UserData); 162 | #pragma warning restore CS0618 // Type or member is obsolete 163 | Console.WriteLine(text); 164 | 165 | var input = plugin.ReadString(new nint(inputs[0].v.ptr)); 166 | Console.WriteLine($"Input: {input}"); 167 | 168 | var output = new string(input); // clone the string 169 | outputs[0].v.ptr = plugin.WriteString(output); 170 | } 171 | } 172 | 173 | [Fact] 174 | public void CountVowelsHostFunctions() 175 | { 176 | for (int i = 0; i < 100; i++) 177 | { 178 | var userData = "Hello again!"; 179 | 180 | using var helloWorld = new HostFunction( 181 | "hello_world", 182 | new[] { ExtismValType.PTR }, 183 | new[] { ExtismValType.PTR }, 184 | userData, 185 | HelloWorld); 186 | 187 | using var plugin = Helpers.LoadPlugin("code-functions.wasm", config: null, helloWorld); 188 | 189 | var dict = new Dictionary 190 | { 191 | { "answer", 42 } 192 | }; 193 | 194 | var response = plugin.CallWithHostContext("count_vowels", Encoding.UTF8.GetBytes("Hello World"), dict); 195 | Encoding.UTF8.GetString(response).ShouldBe("{\"count\": 3}"); 196 | } 197 | 198 | void HelloWorld(CurrentPlugin plugin, Span inputs, Span outputs) 199 | { 200 | Console.WriteLine("Hello from .NET!"); 201 | 202 | var text = plugin.GetUserData(); 203 | Assert.Equal("Hello again!", text); 204 | 205 | var context = plugin.GetCallHostContext>(); 206 | if (context is null || !context.ContainsKey("answer")) 207 | { 208 | throw new InvalidOperationException("Context not found"); 209 | } 210 | 211 | Assert.Equal(42, context["answer"]); 212 | 213 | var input = plugin.ReadString(new nint(inputs[0].v.ptr)); 214 | Console.WriteLine($"Input: {input}"); 215 | 216 | var output = new string(input); // clone the string 217 | outputs[0].v.ptr = plugin.WriteString(output); 218 | } 219 | } 220 | 221 | [Fact] 222 | public void CountVowelsHostFunctionsNoUserData() 223 | { 224 | for (int i = 0; i < 100; i++) 225 | { 226 | using var helloWorld = new HostFunction( 227 | "hello_world", 228 | new[] { ExtismValType.PTR }, 229 | new[] { ExtismValType.PTR }, 230 | null, 231 | HelloWorld); 232 | 233 | using var plugin = Helpers.LoadPlugin("code-functions.wasm", config: null, helloWorld); 234 | 235 | var dict = new Dictionary 236 | { 237 | { "answer", 42 } 238 | }; 239 | 240 | var response = plugin.Call("count_vowels", Encoding.UTF8.GetBytes("Hello World")); 241 | Encoding.UTF8.GetString(response).ShouldBe("{\"count\": 3}"); 242 | } 243 | 244 | void HelloWorld(CurrentPlugin plugin, Span inputs, Span outputs) 245 | { 246 | Console.WriteLine("Hello from .NET!"); 247 | 248 | var text = plugin.GetUserData(); 249 | Assert.Null(text); 250 | 251 | var input = plugin.ReadString(new nint(inputs[0].v.ptr)); 252 | Console.WriteLine($"Input: {input}"); 253 | 254 | var output = new string(input); // clone the string 255 | outputs[0].v.ptr = plugin.WriteString(output); 256 | } 257 | } 258 | 259 | [Fact] 260 | public void HostFunctionsWithMemory() 261 | { 262 | var userData = Marshal.StringToHGlobalAnsi("Hello again!"); 263 | 264 | using var helloWorld = HostFunction.FromMethod("to_upper", IntPtr.Zero, (CurrentPlugin plugin, long offset) => 265 | { 266 | var input = plugin.ReadString(offset); 267 | var output = input.ToUpperInvariant(); 268 | Console.WriteLine($"Result: {output}"); ; 269 | plugin.FreeBlock(offset); 270 | 271 | return plugin.WriteString(output); 272 | }).WithNamespace("host"); 273 | 274 | using var plugin = Helpers.LoadPlugin("host_memory.wasm", config: null, helloWorld); 275 | 276 | var response = plugin.Call("run_test", Encoding.UTF8.GetBytes("Frodo")); 277 | Encoding.UTF8.GetString(response).ShouldBe("HELLO FRODO!"); 278 | } 279 | 280 | [Fact] 281 | public void FuelLimit() 282 | { 283 | using var plugin = Helpers.LoadPlugin("loop.wasm", options: new PluginIntializationOptions 284 | { 285 | FuelLimit = 1000, 286 | WithWasi = true 287 | }); 288 | 289 | Should.Throw(() => plugin.Call("loop_forever", Array.Empty())) 290 | .Message.ShouldContain("fuel"); 291 | } 292 | 293 | //[Fact] 294 | // flakey 295 | internal void FileLog() 296 | { 297 | var tempFile = Path.GetTempFileName(); 298 | Plugin.ConfigureFileLogging(tempFile, LogLevel.Warn); 299 | using (var plugin = Helpers.LoadPlugin("log.wasm")) 300 | { 301 | plugin.Call("run_test", Array.Empty()); 302 | } 303 | 304 | // HACK: tempFile gets locked by the Extism runtime 305 | var tempFile2 = Path.GetTempFileName(); 306 | File.Copy(tempFile, tempFile2, true); 307 | 308 | var content = File.ReadAllText(tempFile2); 309 | content.ShouldContain("warn"); 310 | content.ShouldContain("error"); 311 | content.ShouldNotContain("info"); 312 | content.ShouldNotContain("debug"); 313 | content.ShouldNotContain("trace"); 314 | } 315 | 316 | 317 | // [Fact] 318 | // Interferes with FileLog 319 | internal void CustomLog() 320 | { 321 | var builder = new StringBuilder(); 322 | 323 | Plugin.ConfigureCustomLogging(LogLevel.Warn); 324 | using (var plugin = Helpers.LoadPlugin("log.wasm")) 325 | { 326 | plugin.Call("run_test", Array.Empty()); 327 | } 328 | 329 | Plugin.DrainCustomLogs(line => builder.AppendLine(line)); 330 | 331 | var content = builder.ToString(); 332 | content.ShouldContain("warn"); 333 | content.ShouldContain("error"); 334 | content.ShouldNotContain("info"); 335 | content.ShouldNotContain("debug"); 336 | content.ShouldNotContain("trace"); 337 | } 338 | 339 | [Fact] 340 | public void F64Return() 341 | { 342 | using var plugin = Helpers.LoadPlugin("float.wasm", config: null, HostFunctions()); 343 | 344 | var response = plugin.Call("addf64", Array.Empty()); 345 | var result = BitConverter.ToDouble(response); 346 | result.ShouldBe(101.5); 347 | } 348 | 349 | [Fact] 350 | public void F32Return() 351 | { 352 | using var plugin = Helpers.LoadPlugin("float.wasm", config: null, HostFunctions()); 353 | 354 | var response = plugin.Call("addf32", Array.Empty()); 355 | var result = BitConverter.ToSingle(response); 356 | result.ShouldBe(101.5f); 357 | } 358 | 359 | private HostFunction[] HostFunctions() 360 | { 361 | var functions = new HostFunction[] 362 | { 363 | HostFunction.FromMethod("getf64", IntPtr.Zero, (CurrentPlugin plugin) => 364 | { 365 | return 100.5; 366 | }), 367 | 368 | HostFunction.FromMethod("getf32", IntPtr.Zero, (CurrentPlugin plugin) => 369 | { 370 | return 100.5f; 371 | }), 372 | }; 373 | 374 | foreach (var function in functions) 375 | { 376 | function.SetNamespace("example"); 377 | } 378 | 379 | return functions; 380 | } 381 | 382 | public class CountVowelsResponse 383 | { 384 | public int Count { get; set; } 385 | public int Total { get; set; } 386 | public string? Vowels { get; set; } 387 | } 388 | } 389 | -------------------------------------------------------------------------------- /test/Extism.Sdk/CompiledPluginTests.cs: -------------------------------------------------------------------------------- 1 | using Shouldly; 2 | 3 | using System.Runtime.InteropServices; 4 | using System.Text; 5 | 6 | using Xunit; 7 | 8 | using static Extism.Sdk.Tests.BasicTests; 9 | 10 | namespace Extism.Sdk.Tests; 11 | 12 | public class CompiledPluginTests 13 | { 14 | [Fact] 15 | public void CountVowels() 16 | { 17 | using var compiledPlugin = Helpers.CompilePlugin("code.wasm"); 18 | 19 | for (var i = 0; i < 3; i++) 20 | { 21 | using var plugin = compiledPlugin.Instantiate(); 22 | 23 | var response = plugin.Call("count_vowels", "Hello World"); 24 | 25 | response.ShouldNotBeNull(); 26 | response.Count.ShouldBe(3); 27 | } 28 | } 29 | 30 | [Fact] 31 | public void CountVowelsHostFunctions() 32 | { 33 | var userData = "Hello again!"; 34 | using var helloWorld = HostFunction.FromMethod("hello_world", userData, HelloWorld); 35 | 36 | using var compiledPlugin = Helpers.CompilePlugin("code-functions.wasm", null, helloWorld); 37 | for (int i = 0; i < 3; i++) 38 | { 39 | using var plugin = compiledPlugin.Instantiate(); 40 | 41 | var response = plugin.Call("count_vowels", Encoding.UTF8.GetBytes("Hello World")); 42 | Encoding.UTF8.GetString(response).ShouldBe("{\"count\": 3}"); 43 | } 44 | 45 | long HelloWorld(CurrentPlugin plugin, long ptr) 46 | { 47 | Console.WriteLine("Hello from .NET!"); 48 | 49 | var text = plugin.GetUserData(); 50 | Console.WriteLine(text); 51 | 52 | var input = plugin.ReadString(ptr); 53 | Console.WriteLine($"Input: {input}"); 54 | 55 | return plugin.WriteString(new string(input)); // clone the string 56 | } 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /test/Extism.Sdk/Extism.Sdk.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | false 8 | True 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | all 19 | 20 | 21 | runtime; build; native; contentfiles; analyzers; buildtransitive 22 | all 23 | 24 | 25 | 26 | 27 | 28 | PreserveNewest 29 | 30 | 31 | 32 | PreserveNewest 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /test/Extism.Sdk/Helpers.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace Extism.Sdk.Tests; 4 | 5 | public static class Helpers 6 | { 7 | public static Plugin LoadPlugin(string name, PluginIntializationOptions options, Action? config = null, params HostFunction[] hostFunctions) 8 | { 9 | var binDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!; 10 | var manifest = new Manifest(new PathWasmSource(Path.Combine(binDirectory, "wasm", name), "main")); 11 | 12 | if (config is not null) 13 | { 14 | config(manifest); 15 | } 16 | 17 | return new Plugin(manifest, hostFunctions, options); 18 | } 19 | 20 | public static Plugin LoadPlugin(string name, Action? config = null, params HostFunction[] hostFunctions) 21 | { 22 | return LoadPlugin(name, new PluginIntializationOptions 23 | { 24 | WithWasi = true, 25 | }, config, hostFunctions); 26 | } 27 | 28 | public static CompiledPlugin CompilePlugin(string name, Action? config = null, params HostFunction[] hostFunctions) 29 | { 30 | var binDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!; 31 | var manifest = new Manifest(new PathWasmSource(Path.Combine(binDirectory, "wasm", name), "main")); 32 | if (config is not null) 33 | { 34 | config(manifest); 35 | } 36 | 37 | return new CompiledPlugin(manifest, hostFunctions, withWasi: true); 38 | } 39 | } -------------------------------------------------------------------------------- /test/Extism.Sdk/ManifestTests.cs: -------------------------------------------------------------------------------- 1 | using Shouldly; 2 | using System.Reflection; 3 | using System.Text; 4 | 5 | using Xunit; 6 | 7 | namespace Extism.Sdk.Tests; 8 | 9 | public class ManifestTests 10 | { 11 | [Fact] 12 | public void LoadPluginFromByteArray() 13 | { 14 | var binDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!; 15 | var wasm = File.ReadAllBytes(Path.Combine(binDirectory, "wasm", "code.wasm")); 16 | 17 | var manifest = new Manifest(new ByteArrayWasmSource(wasm, "main")); 18 | 19 | using var plugin = new Plugin(manifest, Array.Empty(), withWasi: true); 20 | 21 | var response = plugin.Call("count_vowels", Encoding.UTF8.GetBytes("Hello World")); 22 | Encoding.UTF8.GetString(response).ShouldContain("\"count\":3"); 23 | } 24 | 25 | [Fact] 26 | public void LoadPluginFromPath() 27 | { 28 | var binDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!; 29 | var manifest = new Manifest(new PathWasmSource(Path.Combine(binDirectory, "wasm", "code.wasm"), "main")); 30 | 31 | using var plugin = new Plugin(manifest, Array.Empty(), withWasi: true); 32 | 33 | var response = plugin.Call("count_vowels", Encoding.UTF8.GetBytes("Hello World")); 34 | Encoding.UTF8.GetString(response).ShouldContain("\"count\":3"); 35 | } 36 | 37 | [Fact] 38 | public void LoadPluginFromUri() 39 | { 40 | var source = new UrlWasmSource("https://raw.githubusercontent.com/extism/extism/main/wasm/code.wasm") 41 | { 42 | Method = HttpMethod.GET, 43 | Headers = new Dictionary 44 | { 45 | { "Authorization", "Basic " } 46 | } 47 | }; 48 | 49 | var manifest = new Manifest(source); 50 | 51 | using var plugin = new Plugin(manifest, Array.Empty(), withWasi: true); 52 | 53 | var response = plugin.Call("count_vowels", Encoding.UTF8.GetBytes("Hello World")); 54 | Encoding.UTF8.GetString(response).ShouldContain("\"count\":3"); 55 | } 56 | 57 | [Theory] 58 | [InlineData("hello", "{\"config\": \"hello\"}")] 59 | [InlineData("", "{\"config\": \"\"}")] 60 | public void CanSetPluginConfig(string thing, string expected) 61 | { 62 | using var plugin = Helpers.LoadPlugin("config.wasm", m => 63 | { 64 | if (!string.IsNullOrEmpty(thing)) 65 | { 66 | m.Config["thing"] = thing; 67 | } 68 | }); 69 | 70 | var response = plugin.Call("run_test", Array.Empty()); 71 | var actual = Encoding.UTF8.GetString(response); 72 | 73 | actual.ShouldBe(expected); 74 | } 75 | 76 | [Fact] 77 | public void CanMakeHttpCalls_WhenAllowed() 78 | { 79 | using var plugin = Helpers.LoadPlugin("http.wasm", m => 80 | { 81 | m.AllowedHosts.Add("jsonplaceholder.*.com"); 82 | }); 83 | 84 | var expected = 85 | """ 86 | { 87 | "userId": 1, 88 | "id": 1, 89 | "title": "delectus aut autem", 90 | "completed": false 91 | } 92 | """; 93 | 94 | var response = plugin.Call("run_test", Array.Empty()); 95 | var actual = Encoding.UTF8.GetString(response); 96 | actual.ShouldBe(expected, StringCompareShould.IgnoreLineEndings); 97 | } 98 | 99 | [Theory] 100 | [InlineData("")] 101 | [InlineData("google*")] 102 | public void CantMakeHttpCalls_WhenDenied(string allowedHost) 103 | { 104 | using var plugin = Helpers.LoadPlugin("http.wasm", m => 105 | { 106 | if (!string.IsNullOrEmpty(allowedHost)) 107 | { 108 | m.AllowedHosts.Add(allowedHost); 109 | } 110 | }); 111 | 112 | Should.Throw(() => plugin.Call("run_test", Array.Empty())); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /test/Extism.Sdk/data/test.txt: -------------------------------------------------------------------------------- 1 | hello world! -------------------------------------------------------------------------------- /toc.yml: -------------------------------------------------------------------------------- 1 | - name: Home 2 | href: index.md 3 | 4 | - name: API 5 | href: api/ -------------------------------------------------------------------------------- /wasm/alloc.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extism/dotnet-sdk/629608a18d3232dabe0b81a7ddd1a13d902e989f/wasm/alloc.wasm -------------------------------------------------------------------------------- /wasm/code-functions.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extism/dotnet-sdk/629608a18d3232dabe0b81a7ddd1a13d902e989f/wasm/code-functions.wasm -------------------------------------------------------------------------------- /wasm/code.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extism/dotnet-sdk/629608a18d3232dabe0b81a7ddd1a13d902e989f/wasm/code.wasm -------------------------------------------------------------------------------- /wasm/config.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extism/dotnet-sdk/629608a18d3232dabe0b81a7ddd1a13d902e989f/wasm/config.wasm -------------------------------------------------------------------------------- /wasm/exit.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extism/dotnet-sdk/629608a18d3232dabe0b81a7ddd1a13d902e989f/wasm/exit.wasm -------------------------------------------------------------------------------- /wasm/fail.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extism/dotnet-sdk/629608a18d3232dabe0b81a7ddd1a13d902e989f/wasm/fail.wasm -------------------------------------------------------------------------------- /wasm/float.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extism/dotnet-sdk/629608a18d3232dabe0b81a7ddd1a13d902e989f/wasm/float.wasm -------------------------------------------------------------------------------- /wasm/fs.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extism/dotnet-sdk/629608a18d3232dabe0b81a7ddd1a13d902e989f/wasm/fs.wasm -------------------------------------------------------------------------------- /wasm/globals.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extism/dotnet-sdk/629608a18d3232dabe0b81a7ddd1a13d902e989f/wasm/globals.wasm -------------------------------------------------------------------------------- /wasm/host_memory.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extism/dotnet-sdk/629608a18d3232dabe0b81a7ddd1a13d902e989f/wasm/host_memory.wasm -------------------------------------------------------------------------------- /wasm/http.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extism/dotnet-sdk/629608a18d3232dabe0b81a7ddd1a13d902e989f/wasm/http.wasm -------------------------------------------------------------------------------- /wasm/kitchensink.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extism/dotnet-sdk/629608a18d3232dabe0b81a7ddd1a13d902e989f/wasm/kitchensink.wasm -------------------------------------------------------------------------------- /wasm/log.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extism/dotnet-sdk/629608a18d3232dabe0b81a7ddd1a13d902e989f/wasm/log.wasm -------------------------------------------------------------------------------- /wasm/loop.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extism/dotnet-sdk/629608a18d3232dabe0b81a7ddd1a13d902e989f/wasm/loop.wasm -------------------------------------------------------------------------------- /wasm/sleep.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extism/dotnet-sdk/629608a18d3232dabe0b81a7ddd1a13d902e989f/wasm/sleep.wasm -------------------------------------------------------------------------------- /wasm/var.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extism/dotnet-sdk/629608a18d3232dabe0b81a7ddd1a13d902e989f/wasm/var.wasm --------------------------------------------------------------------------------