Skip to content
Back to search

星光 · F# 加载器

by HosihikariFramework

星光框架的 F# 型插件加载器:装载时编译脚本,交给 collectible ALC

  • Updated
1.0.0

README1.0.0

Hosihikari.PluginLoader.FSharp

English | 简体中文

The F# plugin loader of the Hosihikari framework. F# runs on the CLR, so this loader compiles at load time: a .fsx becomes a real assembly, which then goes into its own collectible ALC.

That buys F# plugins the same promises as any assembly plugin: per-plugin dependency isolation, and an unload that is verified rather than assumed. This is an in-process loader: it compiles source to IL before the plugin runs, and the loader loads that IL into a collectible ALC.

In-process .NET. The framework keeps the assembly loader, the C# loader and this F# loader in-process; other languages go through the generic out-of-process loader (Hosihikari.PluginLoader.Rpc, ADR-0031).

Templates

Start from FSharpPluginTemplate: copy it into plugins/, rename the plugin, and restart.

What a plugin package looks like

plugins/Hello/
  manifest.json
    {
      "name": "Hello",
      "entry": "main.fsx",
      "type": "hosihikari",
      "version": "1.0.0",
      "dependencies": [
        { "name": "hosihikari" },
        { "name": "hosihikari-fsharp" }
      ],
      "extraInfo": { "api_version": "2", "manifest_version": "1", "loader": "fsharp" }
    }
  main.fsx
  Hello.conf          ← the author's defaults, shipped with the package

The source defines a type implementing IPlugin with a parameterless constructor; finding it is the loader's job. Annotate the hook parameter — the compiler needs it:

open Hosihikari
open System.IO

type HelloPlugin() =
    let mutable dataDirectory = ""

    interface IPlugin with
        member this.OnLoad(context: IPluginContext) =
            dataDirectory <- context.DataDirectory
            let greeting = context.Configuration.["greeting"]
            File.WriteAllText(Path.Combine(dataDirectory, "body.txt"), "body:" + greeting)
        member this.OnEnable() =
            File.WriteAllText(Path.Combine(dataDirectory, "enabled.txt"), "enabled")
            PluginHookResult.Allow()
        member this.OnDisable() = PluginHookResult.Allow()
        member this.OnUnload() = ()

Because the result is a real assembly referencing the contract, an F# plugin reaches everything a C# plugin does — including the type-keyed channels (event bus, service registry) that an out-of-process plugin cannot.

How it compiles

FSharpChecker from FSharp.Compiler.Service, driven explicitly rather than through GetProjectOptionsFromScript — in a directory with no SDK context that helper falls back to the .NET Framework reference set. The loader passes --noframework --targetprofile:netcore plus an explicit reference list: assemblies already loaded in the framework's own ALC (the contract, even when it is absent from the TPA), private DLLs in the plugin package root, the loader's own directory (which ships FSharp.Core and friends), and finally the host's trusted platform assemblies. The contract comes from the host, so it keeps its identity.

The output is cached by content: <data directory>/bin/<hash>/<name>.dll, the hash covering the source text, the files pulled in by #load and local #r directives, package-local private DLL names and contents, the compiler version and the runtime version. A checksum sits next to it and is verified before loading — a mismatch means recompile, never load anyway. Compilation goes into a staging directory and is moved into place only on success.

Entries whose directives resolve to local files (#load, #r with a path) are cached like any other entry; editing a helper produces a new directory. Entries using #I, nuget: references, globs or unresolved paths are recompiled on every load into a unique -run- directory. A # in comments or ordinary strings is not a directive and no longer forces recompilation.

The checksum guards against accidents (a swapped or half-written artifact, a compiler or runtime change), not against a hostile plugin: plugins share this process and are trusted (ADR-0011), so one could forge the cache as well. ADR-0029 records that boundary.

Running it

dotnet run --project host/Hosihikari.PluginLoader.FSharp.Host.csproj -c Release

The harness drives this loader through the real PluginManager, so sibling Hosihikari.PluginManager, the contract checkout (package Hosihikari.Plugin.Abstractions) and the McMaster fork checkouts are required. It checks these things: the loader is registered by a plugin package, the artifact lands in a content-addressed directory with its checksum and PDB, the source is compiled at load and reads the operator's override, enable hooks reach it, a channel message from the host reaches it, a package-local third-party dependency is usable, disable and unload hooks arrive, the unload verifies (the ALC is actually collected), a second load hits the cache, and a tampered artifact is detected and recompiled. It also uses a private fixture DLL that the host does not reference, and checks that editing only a #load helper changes the next load while both loaded instances can still be collected. Directive caching is covered as well: helpers and multi-file #load participate in the cache key, an unchanged directive entry hits the cache, and an #I entry rotates directories. A comment-prefixed directive is still recognized, #load text inside a multiline string is treated as text, and the (*) operator is not mistaken for a comment. Strings inside comments, quoted identifiers containing (*, and character literals containing a quote are covered too.

How it reaches the host process

As a plugin package, named hosihikari-fsharp — the same way as the C# loader. Source packages declare the dependency, and the dependency graph orders the loader first:

"dependencies": [
  { "name": "hosihikari" },
  { "name": "hosihikari-fsharp" }
]

Registration cannot be undone: the types this loader hands out live in its load context.

Packaging

pwsh ./pack.ps1 -Version 1.0.0 -OutputRoot artifacts

It assembles the loader DLL, its private dependencies and the manifest into hosihikari-fsharp/; the shared layer is not copied. Pushing a v* tag attaches the zip to the GitHub release (ADR-0036).

The script invokes the core packager (Hosihikari.PluginManager/tools/Pack), which owns the shared-layer list. Check out the core repository alongside this one, or set HOSIHIKARI_PLUGINMANAGER.

After release: lip install github.com/HosihikariFramework/FSharpLoader (install the Shim variant for the carrier first).

Licence

LGPL-3.0-or-later, see LICENSE.md.

Install 星光 · F# 加载器

Run this in your server or client folder with lip:

lip install github.com/HosihikariFramework/FSharpLoader@1.0.0
Download from GitHub