Skip to content

C# plugins

C# runs on the CLR, so Hosihikari.PluginLoader.CSharp compiles .cs at load with Roslyn and loads the assembly into a collectible ALC. Authors ship source; the result gets the same isolation and unload guarantees as any assembly plugin. This is an in-process loader: it compiles source AOT to IL before the plugin runs and loads that IL into a collectible ALC; the plugin itself is ordinary CLR code.

Pick the HostingShim variant for the server (see Choosing a carrier); the examples below use LeviLamina:

Terminal window
lip install github.com/HosihikariFramework/HostingShim#levilamina
lip install github.com/HosihikariFramework/CSharpLoader

The C# template is a complete package you can copy and rename.

plugins/hello-csharp/
manifest.json
main.cs
hello-csharp.conf

Manifest:

{
"name": "hello-csharp",
"entry": "main.cs",
"type": "hosihikari",
"version": "1.0.0",
"dependencies": [
{ "name": "hosihikari" },
{ "name": "hosihikari-csharp" }
],
"extraInfo": { "api_version": "2", "manifest_version": "1", "loader": "csharp" }
}

The entry file accepts two directives from .NET file-based apps. They are replaced with blank lines before Roslyn compiles, and the packer acts on them:

  • #:package [email protected] restores the package at pack time into the plugin package. The server never restores or downloads packages when it loads a plugin.
  • #:include helpers.cs compiles another .cs file from the package with the entry. Paths are relative to the including file, may be quoted, and may use .. to reach sibling directories inside the package. *, **, and ? globs are supported, for example #:include "../shared/*.cs".

#:project, #:property, #:sdk, and top-level statements are not supported. The entry still defines an IPlugin type. For a standalone run loop, use a normal file-based app or a project, then package the compiled assembly with the precompiled template.

Define a type that implements IPlugin and has a parameterless constructor. The loader finds it and creates an instance.

using System.IO;
using Hosihikari;
using Microsoft.Extensions.Logging;
public sealed class HelloPlugin : IPlugin
{
private IPluginContext _context = null!;
public void OnLoad(IPluginContext context)
{
_context = context;
context.Logger.LogInformation("loaded, package {Dir}", context.PackageDirectory);
File.WriteAllText(Path.Combine(context.DataDirectory, "loaded.txt"), "loaded");
}
public PluginHookResult OnEnable()
{
var greeting = _context.Configuration["greeting"];
_context.Logger.LogInformation("enabled, greeting={Greeting}", greeting);
File.WriteAllText(Path.Combine(_context.DataDirectory, "enabled.txt"), greeting ?? "");
return PluginHookResult.Allow();
}
public PluginHookResult OnDisable() => PluginHookResult.Allow();
public void OnUnload() { }
}

using Microsoft.Extensions.Logging; is required: LogInformation is an extension method.

  • Compiled at load; a source plugin needs no csproj.
  • Output is content-addressed under data/hosihikari/<name>/bin/<hash>/, next to a checksum and a portable PDB.
  • A change to the source, a private DLL, the Roslyn version, or the runtime version changes the hash.
  • Private DLLs in the package root participate in compilation references and are resolved from the package at run time.
  • Native libraries live in the package root or runtimes/<current portable RID>/native/; the RID directory wins.

The package default hello-csharp.conf:

greeting = "Hello, Hosihikari"

The server override is config/hosihikari/hello-csharp.conf; environment variables also override the same keys.

context.Events and context.Scheduler come from the framework. Disabling closes the gate first, so event and timer callbacks stop; start periodic work in OnEnable.

If you already have a compiled DLL, put it in the package, set loader to assembly, and point entry at the DLL. Declare the entry on the assembly:

[assembly: EntryPoint<HelloPlugin>]

Reference the contract and exclude its runtime assets so the package does not carry a second copy:

<PackageReference Include="Hosihikari.Plugin.Abstractions" Version="[1.1.0]" ExcludeAssets="runtime" />

The assembly declares exactly one EntryPoint<T>; the entry type implements IPlugin and has a parameterless constructor.

Put the directory under plugins/ and start the server. The log should show loaded, package and enabled; loaded.txt and enabled.txt appear in the data directory. On unload the plugin manager uses the plugin's PluginHandle: Unload() / WaitForUnloadAsync() release it, and IsUnloaded verifies reclamation after the ALC is dropped.