{"url":"https://levimc.org/mods/github.com/HosihikariFramework/FSharpProvider/","id":"github.com/HosihikariFramework/FSharpProvider","name":"星光 · F# Provider","description":"星光框架的 F# 型插件 Provider：装载时编译脚本，交给 collectible ALC","owner":"HosihikariFramework","logo":"https://levimc.org/mods/icons/7d76cf41a7985a82-288.webp","repository":"https://github.com/HosihikariFramework/FSharpLoader","issues":"https://github.com/HosihikariFramework/FSharpLoader/issues","releases":"https://github.com/HosihikariFramework/FSharpLoader/releases","stars":0,"downloads":102,"updated":"2026-11-09T10:49:24Z","published":"2026-10-19T04:41:04Z","created":"2026-10-17T03:58:13Z","license":"LGPL-3.0","side":["server","client"],"platform":["android-arm64","linux-arm64","linux-x64","win-arm64","win-x64"],"gameVersions":[],"tags":["platform:levilamina","platform:endstone","type:mod"],"latest":{"variant":"","version":"0.1.5","install":"lip install github.com/HosihikariFramework/FSharpProvider@0.1.5"},"readmes":[{"file":"README.md","lang":"en","text":"# Hosihikari.PluginProvider.FSharp\n\n[English](README.md) | [简体中文](README.zh_hans.md)\n\nThe **F# plugin Provider** of the Hosihikari framework. F# runs on the CLR, so this provider\n**compiles at load time**: a `.fsx` becomes a real assembly, which then goes\ninto its own collectible ALC.\n\nThat buys F# plugins the same promises as any assembly plugin: per-plugin dependency isolation, and\nan unload that is verified rather than assumed. This is a first-class provider: it AOT-compiles source\nto IL before the plugin runs, and the provider loads that IL into a collectible ALC.\n\n**In-process .NET.** The framework keeps the assembly provider, the C# provider and this F# provider\nin-process; other languages go through the generic out-of-process provider\n(`Hosihikari.PluginProvider.Rpc`, ADR-0031).\n\n## Templates\n\nStart from [FSharpPluginTemplate](https://github.com/HosihikariFramework/FSharpPluginTemplate): copy\nit into `plugins/`, rename the plugin, and restart.\n\n## What a plugin package looks like\n\n```\nplugins/Hello/\n  manifest.json\n    {\n      \"name\": \"Hello\",\n      \"entry\": \"main.fsx\",\n      \"type\": \"hosihikari\",\n      \"version\": \"1.0.0\",\n      \"dependencies\": [\n        { \"name\": \"hosihikari\" },\n        { \"name\": \"hosihikari-fsharp\" }\n      ],\n      \"extraInfo\": { \"api\": \"2\", \"format_version\": \"1\", \"provider\": \"fsharp\" }\n    }\n  main.fsx\n  Hello.conf          ← the author's defaults, shipped with the package\n```\n\nThe source defines a type implementing `IPlugin` with a parameterless constructor; finding it is the\nprovider's job. Annotate the hook parameter — the compiler needs it:\n\n```fsharp\nopen Hosihikari\nopen System.IO\n\ntype HelloPlugin() =\n    let mutable dataDirectory = \"\"\n\n    interface IPlugin with\n        member this.OnLoad(context: IPluginContext) =\n            dataDirectory <- context.DataDirectory\n            let greeting = context.Configuration.[\"greeting\"]\n            File.WriteAllText(Path.Combine(dataDirectory, \"body.txt\"), \"body:\" + greeting)\n        member this.OnEnable() =\n            File.WriteAllText(Path.Combine(dataDirectory, \"enabled.txt\"), \"enabled\")\n            PluginHookResult.Allow()\n        member this.OnDisable() = PluginHookResult.Allow()\n        member this.OnUnload() = ()\n```\n\nBecause the result is a real assembly referencing the contract, an F# plugin reaches everything a C#\nplugin does — including the type-keyed channels (event bus, service registry) that an out-of-process\nplugin cannot.\n\n## How it compiles\n\n`FSharpChecker` from `FSharp.Compiler.Service`, driven explicitly rather than through\n`GetProjectOptionsFromScript` — in a directory with no SDK context that helper falls back to the\n.NET Framework reference set. The provider passes `--noframework --targetprofile:netcore` plus an\nexplicit reference list: the host's trusted platform assemblies, and the provider's own directory\n(which ships `FSharp.Core` and friends). The contract comes from the host, so it keeps its identity.\nPrivate DLLs in the plugin package root are also referenced at compile time and resolved from that\ndirectory at runtime.\n\nThe output is cached by content: `<data directory>/bin/<hash>/<name>.dll`, the hash covering the\nsource text, the files pulled in by `#load` and local `#r` directives, package-local private DLL\nnames and contents, the compiler version and the runtime version. A checksum sits next to it and is\nverified before loading — a mismatch means recompile, never load anyway. Compilation goes into a\nstaging directory and is moved into place only on success.\n\nEntries whose directives resolve to local files (`#load`, `#r` with a path) are cached like any\nother entry; editing a helper produces a new directory. Entries using `#I`, `nuget:` references,\nglobs or unresolved paths are recompiled on every load into a unique `-run-` directory. A `#` in\ncomments or ordinary strings is not a directive and no longer forces recompilation.\n\nThe checksum guards against accidents (a swapped or half-written artifact, a compiler or runtime\nchange), not against a hostile plugin: plugins share this process and are trusted (ADR-0011), so one\ncould forge the cache as well. ADR-0029 records that boundary.\n\n## Running it\n\n```\ndotnet run --project host/Hosihikari.PluginProvider.FSharp.Host.csproj -c Release\n```\n\nThe harness drives this Provider through the core's real `PluginManagerHost`, so sibling\n`Hosihikari.PluginManager`, `Hosihikari.Abstractions` and McMaster fork checkouts are required. It checks these\nthings: the Provider is registered by a plugin package, the artifact lands in a content-addressed\ndirectory with its checksum and PDB, the source is compiled at load and reads the operator's override,\nenable hooks reach it, a channel message from the host reaches it, a package-local third-party\ndependency is usable, disable and unload hooks arrive, the unload verifies (the ALC is actually\ncollected), a second load hits the cache, and a tampered artifact is detected and recompiled.\nIt also uses a private fixture DLL that the host does not reference, and checks that editing only a\n`#load` helper changes the next load while both loaded instances can still be collected. Directive\ncaching is covered as well: helpers and multi-file `#load` participate in the cache key, an\nunchanged directive entry hits the cache, and an `#I` entry rotates directories. A\ncomment-prefixed directive is still recognized, `#load` text inside a multiline string is treated\nas text, and the `(*)` operator is not mistaken for a comment. Strings inside comments, quoted\nidentifiers containing `(*`, and character literals containing a quote are covered too.\n\n## How it reaches the host process\n\nAs a plugin package, named `hosihikari-fsharp` — the same way as the C# provider. Source packages\ndeclare the dependency, and the dependency graph orders the provider first:\n\n```json\n\"dependencies\": [\n  { \"name\": \"hosihikari\" },\n  { \"name\": \"hosihikari-fsharp\" }\n]\n```\n\n**Registration cannot be undone**: the types this provider hands out live in its load context.\n\n## Packaging\n\n```\npwsh ./pack.ps1 -Version 0.1.5 -OutputRoot artifacts\n```\n\nIt assembles the provider DLL, its private dependencies and the manifest into\n`hosihikari-fsharp/`; the shared layer is not copied. Pushing a `v*` tag attaches the zip to the\nGitHub release (ADR-0036).\n\nThe script invokes the core packager (`Hosihikari.PluginManager/tools/Pack`), which owns the\nshared-layer list. Check out the core repository alongside this one, or set\n`HOSIHIKARI_PLUGINMANAGER`.\n\nAfter release: `lip install github.com/HosihikariFramework/FSharpProvider` (install the Shim\nvariant for the carrier first).\n\n## Licence\n\nLGPL-3.0-or-later, see [LICENSE.md](LICENSE.md).\n"},{"file":"README.zh_hans.md","lang":"zh-Hans","text":"# Hosihikari.PluginProvider.FSharp\n\n[English](README.md) | [简体中文](README.zh_hans.md)\n\n星光框架的 **F# 型插件 Provider**。F# 直接跑在 CLR 上，所以这个 Provider\n**装载时编译**：`.fsx` 变成一个真正的程序集，再进它自己的 collectible ALC。\n\n这让 F# 插件拿到与其他程序集插件一样的承诺：每插件的依赖互相隔离，卸载是真的验证过而不是假定的。\n这是一等 Provider：在插件开始运行前把源码 AOT 为 IL，Provider 把 IL 装进 collectible ALC。\n\n**进程内只留 .NET。** 框架在进程内保留程序集型、C# 与 F# 三个 Provider；其他语言走\n通用的进程外 Provider（`Hosihikari.PluginProvider.Rpc`，ADR-0031）。\n\n## 模板\n\n从 [FSharpPluginTemplate](https://github.com/HosihikariFramework/FSharpPluginTemplate) 开始：复制到\n`plugins/`，改名后重启。\n\n## 插件包长什么样\n\n```\nplugins/Hello/\n  manifest.json\n    {\n      \"name\": \"Hello\",\n      \"entry\": \"main.fsx\",\n      \"type\": \"hosihikari\",\n      \"version\": \"1.0.0\",\n      \"dependencies\": [\n        { \"name\": \"hosihikari\" },\n        { \"name\": \"hosihikari-fsharp\" }\n      ],\n      \"extraInfo\": { \"api\": \"2\", \"format_version\": \"1\", \"provider\": \"fsharp\" }\n    }\n  main.fsx\n  Hello.conf          ← 作者给的默认值，随包更新\n```\n\n源码里定义一个实现 `IPlugin`、有无参构造的类型；找到它是 Provider 的事。**钩子参数要标注类型** ——\n编译器需要它：\n\n```fsharp\nopen Hosihikari\nopen System.IO\n\ntype HelloPlugin() =\n    let mutable dataDirectory = \"\"\n\n    interface IPlugin with\n        member this.OnLoad(context: IPluginContext) =\n            dataDirectory <- context.DataDirectory\n            let greeting = context.Configuration.[\"greeting\"]\n            File.WriteAllText(Path.Combine(dataDirectory, \"body.txt\"), \"body:\" + greeting)\n        member this.OnEnable() =\n            File.WriteAllText(Path.Combine(dataDirectory, \"enabled.txt\"), \"enabled\")\n            PluginHookResult.Allow()\n        member this.OnDisable() = PluginHookResult.Allow()\n        member this.OnUnload() = ()\n```\n\n编出来的是一个引用了契约的真程序集，所以 F# 插件拿得到 C# 插件的一切 —— 包括那些**按类型**做键的通道\n（事件总线、服务注册表），那是任何进程外插件都够不着的。\n\n## 它怎么编译\n\n用 `FSharp.Compiler.Service` 的 `FSharpChecker`，但**不走** `GetProjectOptionsFromScript` ——\n在没有 SDK 上下文的目录里它会退到 .NET Framework 的参考集。这里显式给\n`--noframework --targetprofile:netcore` 加一份引用清单：宿主的运行时，以及这个 Provider 自己的目录\n（`FSharp.Core` 等随包走）。契约由宿主提供，因此身份不变。\n包根目录内的私有 DLL 也参与编译引用，运行时从包目录解析。\n\n产物按内容寻址缓存：`<数据目录>/bin/<哈希>/<名字>.dll`，哈希覆盖源码内容、`#load` 与本地 `#r`\n引入的文件、包内私有 DLL 的名字和内容、编译器版本与运行时版本。\n产物旁存一份校验和，加载前对一遍 —— 对不上就重编，绝不照跑。编译先落到临时目录，成功了才改名进去。\n\n指令能解析到本地文件时（`#load`、带路径的 `#r`），入口照常复用缓存；改辅助文件会换到新目录。\n使用 `#I`、`nuget:`、通配符或解析不了的路径时，入口每次加载都编译到独立的 `-run-` 目录。\n注释与普通字符串里的 `#` 不是指令，不再触发重编。\n\n校验和防的是**事故**（产物被换掉或写坏、编译器或运行时换了一版），不防**恶意**：插件与本框架同进程、\n且完全受信（ADR-0011），它真要伪造缓存也做得到。这条边界记在 ADR-0029 里。\n\n## 跑一遍\n\n```\ndotnet run --project host/Hosihikari.PluginProvider.FSharp.Host.csproj -c Release\n```\n\n验证宿主借核心的 `PluginManagerHost` 驱动它，所以需要并列的 `Hosihikari.PluginManager`、`Hosihikari.Abstractions` 与 McMaster 的 fork checkout。它验证：Provider 由插件包登记、产物按内容寻址落盘且带校验和\n与 PDB、源码装载时编译并读到包外覆盖、启用钩子到达插件、宿主发的通道消息它能收到、包内自带的第三方依赖\n可用、停用与卸载钩子到达、卸载验证通过（ALC 真的被回收）、同一份源码再装载命中缓存、以及产物被改坏后\n会被发现并重编。\n测试还使用宿主没有引用的私有 DLL，并验证只修改 `#load` 的辅助源码后重载会执行新代码，两次实例都能回收；\n指令缓存也覆盖：辅助文件与多文件 `#load` 参与缓存键、同一份指令入口命中缓存、`#I` 入口每次换目录；\n注释前缀的指令仍被识别，多行字符串里的 `#load` 视为文本，`(*)` 运算符不会被当成注释；\n注释里的字符串、含 `(*` 的引号标识符与含引号的字符字面量也已覆盖。\n\n## 它怎么进宿主进程\n\n作为插件包，名字是 `hosihikari-fsharp` —— 与 C# 那个同一套。源码包声明依赖，依赖图先排它：\n\n```json\n\"dependencies\": [\n  { \"name\": \"hosihikari\" },\n  { \"name\": \"hosihikari-fsharp\" }\n]\n```\n\n**登记之后不能撤销**：它造出来的类型住在它的装载上下文里。\n\n## 打包\n\n```\npwsh ./pack.ps1 -Version 0.1.5 -OutputRoot artifacts\n```\n\n它把 Provider DLL、私有依赖与清单装进 `hosihikari-fsharp/`，共享层不随包走；打 `v*` 标签时 CI 把 zip 附到 release（ADR-0036）。\n\n打包脚本调用核心打包器（`Hosihikari.PluginManager/tools/Pack`），共享层清单由核心统一给出；需要并列检出核心仓，或设置 `HOSIHIKARI_PLUGINMANAGER`。\n\n发布后：`lip install github.com/HosihikariFramework/FSharpProvider`（先按载体装 Shim）。\n\n## 许可\n\nLGPL-3.0-or-later，见 [LICENSE.md](LICENSE.md)。\n"}],"gallery":[],"versions":[{"version":"0.1.5","prerelease":false,"released":"2026-10-21T06:37:44Z","releaseUrl":"https://github.com/HosihikariFramework/FSharpLoader/releases/tag/v0.1.5","manifestUrl":"https://github.com/HosihikariFramework/FSharpLoader/blob/v0.1.5/tooth.json","info":{"name":"星光 · F# Provider","description":"星光框架的 F# 型插件 Provider：装载时编译脚本，交给 collectible ALC","tags":["platform:levilamina","platform:endstone","type:mod"]},"variants":{"":[{"side":"server","platform":"win-x64","deps":{}},{"side":"client","platform":"win-x64","deps":{}},{"side":"server","platform":"win-arm64","deps":{}},{"side":"client","platform":"win-arm64","deps":{}},{"side":"server","platform":"linux-x64","deps":{}},{"side":"client","platform":"linux-x64","deps":{}},{"side":"server","platform":"linux-arm64","deps":{}},{"side":"client","platform":"linux-arm64","deps":{}},{"side":"server","platform":"android-arm64","deps":{}},{"side":"client","platform":"android-arm64","deps":{}}]}},{"version":"0.1.4","prerelease":false,"released":"2026-10-21T05:08:31Z","releaseUrl":"https://github.com/HosihikariFramework/FSharpLoader/releases/tag/v0.1.4","manifestUrl":"https://github.com/HosihikariFramework/FSharpLoader/blob/v0.1.4/tooth.json","info":{"name":"星光 · F# Provider","description":"星光框架的 F# 型插件 Provider：装载时编译脚本，交给 collectible ALC","tags":["platform:levilamina","platform:endstone","type:mod"]},"variants":{"":[{"side":"server","platform":"win-x64","deps":{}},{"side":"client","platform":"win-x64","deps":{}},{"side":"server","platform":"win-arm64","deps":{}},{"side":"client","platform":"win-arm64","deps":{}},{"side":"server","platform":"linux-x64","deps":{}},{"side":"client","platform":"linux-x64","deps":{}},{"side":"server","platform":"linux-arm64","deps":{}},{"side":"client","platform":"linux-arm64","deps":{}},{"side":"server","platform":"android-arm64","deps":{}},{"side":"client","platform":"android-arm64","deps":{}}]}},{"version":"0.1.3","prerelease":false,"released":"2026-10-21T03:07:21Z","releaseUrl":"https://github.com/HosihikariFramework/FSharpLoader/releases/tag/v0.1.3","manifestUrl":"https://github.com/HosihikariFramework/FSharpLoader/blob/v0.1.3/tooth.json","info":{"name":"星光 · F# Provider","description":"星光框架的 F# 型插件 Provider：装载时编译脚本，交给 collectible ALC","tags":["platform:levilamina","platform:endstone","type:mod"]},"variants":{"":[{"side":"server","platform":"win-x64","deps":{}},{"side":"client","platform":"win-x64","deps":{}},{"side":"server","platform":"win-arm64","deps":{}},{"side":"client","platform":"win-arm64","deps":{}},{"side":"server","platform":"linux-x64","deps":{}},{"side":"client","platform":"linux-x64","deps":{}},{"side":"server","platform":"linux-arm64","deps":{}},{"side":"client","platform":"linux-arm64","deps":{}},{"side":"server","platform":"android-arm64","deps":{}},{"side":"client","platform":"android-arm64","deps":{}}]}},{"version":"0.1.2","prerelease":false,"released":"2026-10-20T09:44:08Z","releaseUrl":"https://github.com/HosihikariFramework/FSharpLoader/releases/tag/v0.1.2","manifestUrl":"https://github.com/HosihikariFramework/FSharpLoader/blob/v0.1.2/tooth.json","info":{"name":"星光 · F# Provider","description":"星光框架的 F# 型插件 Provider：装载时编译脚本，交给 collectible ALC","tags":["platform:levilamina","platform:endstone","type:mod"]},"variants":{"":[{"side":"server","platform":"win-x64","deps":{}},{"side":"client","platform":"win-x64","deps":{}}]}},{"version":"0.1.1","prerelease":false,"released":"2026-10-20T08:02:05Z","releaseUrl":"https://github.com/HosihikariFramework/FSharpLoader/releases/tag/v0.1.1","manifestUrl":"https://github.com/HosihikariFramework/FSharpLoader/blob/v0.1.1/tooth.json","info":{"name":"星光 · F# Provider","description":"星光框架的 F# 型插件 Provider：装载时编译脚本，交给 collectible ALC","tags":["platform:levilamina","platform:endstone","type:mod"]},"variants":{"":[{"side":"server","platform":"win-x64","deps":{}},{"side":"client","platform":"win-x64","deps":{}}]}},{"version":"0.1.0","prerelease":false,"released":"2026-10-19T04:41:04Z","releaseUrl":"https://github.com/HosihikariFramework/FSharpLoader/releases/tag/v0.1.0","manifestUrl":"https://github.com/HosihikariFramework/FSharpLoader/blob/v0.1.0/tooth.json","info":{"name":"星光 · F# Provider","description":"星光框架的 F# 型插件 Provider：装载时编译脚本，交给 collectible ALC","tags":["platform:levilamina","platform:endstone","type:mod"]},"variants":{"":[{"side":"server","platform":"win-x64","deps":{}},{"side":"client","platform":"win-x64","deps":{}}]}}],"dependencies":{},"requiredBy":[]}