├── .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
--------------------------------------------------------------------------------