לדלג לתוכן

פתרון הפרויקט: מארח רכיבים שניתנים לפריקה

נבנה תחילה את הפתרון שמדגים את החוזה המינימלי. הפקודות מיועדות לתיקייה חדשה, וקובצי C# המלאים מופיעים אחריהן. יש למחוק את Class1.cs האוטומטי בפרויקטי הספרייה או להחליף את תוכנו בקוד המתאים.

dotnet new classlib -n Contract -f net10.0
dotnet new classlib -n Plugin -f net10.0
dotnet new console -n Host -f net10.0
dotnet add Plugin reference Contract
dotnet add Host reference Contract

הקובץ Contract/IPlugin.cs מגדיר חוזה בלי להחזיר references לטיפוס פרטי של הרכיב. הקריאה הסינכרונית היא גם גבול ההשאלה של input.

namespace Contract;
public interface IPlugin : IDisposable
{
    string Name { get; }
    void Accumulate(ReadOnlySpan<byte> input);
    ulong Total { get; }
}

הקובץ Plugin/ByteSum.cs מחזיק רק מצב שבבעלות הרכיב. בדיקת disposed מופיעה גם בקריאת התוצאה, כדי ש־Dispose אינו יהיה אירוע קוסמטי בלבד.

using Contract;
namespace Plugin;

public sealed class ByteSum : IPlugin
{
    private bool disposed;
    private ulong total;
    public string Name => "byte-sum-v1";
    public ulong Total { get { Check(); return total; } }
    public void Accumulate(ReadOnlySpan<byte> input)
    {
        Check();
        foreach (byte value in input) total = checked(total + value);
    }
    public void Dispose() => disposed = true;
    private void Check() => ObjectDisposedException.ThrowIf(disposed, this);
}

הקובץ Host/Program.cs מפריד את תקופת השימוש ברכיב מתקופת בדיקת הפריקה. ה־root שמופעל במצב leak הוא תקלה מוזרקת ומוצהרת, ולא חלק מן העיצוב התקין.

using System.Buffers;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Loader;
using Contract;

if (args.Length is < 2 or > 3)
    throw new ArgumentException("plugin.dll input.bin [leak]");
bool leak = args.Length == 3 && args[2] == "leak";
WeakReference probe = Execute(Path.GetFullPath(args[0]), args[1], leak);
Console.WriteLine($"unloaded first={WaitForUnload(probe)}");
ClearRoot();
Console.WriteLine($"unloaded after cleanup={WaitForUnload(probe)}");

[MethodImpl(MethodImplOptions.NoInlining)]
static WeakReference Execute(string pluginPath, string inputPath, bool leak)
{
    var context = new PluginContext(pluginPath);
    var probe = new WeakReference(context, trackResurrection: true);
    try
    {
        Assembly assembly = context.LoadFromAssemblyPath(pluginPath);
        Type type = assembly.GetType("Plugin.ByteSum", throwOnError: true)!;
        if (!typeof(IPlugin).IsAssignableFrom(type))
            throw new InvalidDataException("plugin contract mismatch");
        using IPlugin plugin = (IPlugin)Activator.CreateInstance(type)!;
        using var input = File.OpenRead(inputPath);
        byte[] buffer = ArrayPool<byte>.Shared.Rent(64 * 1024);
        try
        {
            long before = GC.GetAllocatedBytesForCurrentThread();
            int count;
            while ((count = input.Read(buffer.AsSpan(0, 64 * 1024))) != 0)
                plugin.Accumulate(buffer.AsSpan(0, count));
            long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
            Console.WriteLine($"{plugin.Name}: {plugin.Total}, allocated={allocated}");
            if (leak) Roots.Keep = plugin;
        }
        finally { ArrayPool<byte>.Shared.Return(buffer, clearArray: true); }
    }
    finally { context.Unload(); }
    return probe;
}

[MethodImpl(MethodImplOptions.NoInlining)]
static void ClearRoot() => Roots.Keep = null;

static bool WaitForUnload(WeakReference probe)
{
    for (int i = 0; i < 10 && probe.IsAlive; i++)
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
    }
    return !probe.IsAlive;
}

static class Roots { public static object? Keep; }

sealed class PluginContext : AssemblyLoadContext
{
    private readonly AssemblyDependencyResolver resolver;
    private readonly Assembly shared = typeof(IPlugin).Assembly;
    public PluginContext(string path) : base(isCollectible: true)
        => resolver = new AssemblyDependencyResolver(path);
    protected override Assembly? Load(AssemblyName requested)
    {
        AssemblyName known = shared.GetName();
        if (requested.Name == known.Name)
        {
            if (requested.Version != known.Version)
                throw new FileLoadException("unsupported contract version");
            return shared;
        }
        string? path = resolver.ResolveAssemblyToPath(requested);
        return path is null ? null : LoadFromAssemblyPath(path);
    }
    protected override nint LoadUnmanagedDll(string name)
    {
        string? path = resolver.ResolveUnmanagedDllToPath(name);
        return path is null ? 0 : LoadUnmanagedDllFromPath(path);
    }
}

נבנה את שני הפרויקטים וניצור קלט קטן שהתוצאה שלו ידועה. הפקודות מריצות את המארח מתוך תיקיית המעבדה, ולכן הנתיבים היחסיים נשארים ברורים.

dotnet build Plugin -c Release
dotnet build Host -c Release
[IO.File]::WriteAllBytes((Join-Path (Get-Location) 'input.bin'), [byte[]](1,2,3,4))
dotnet run --project Host -c Release --no-build -- Plugin/bin/Release/net10.0/Plugin.dll input.bin
dotnet run --project Host -c Release --no-build -- Plugin/bin/Release/net10.0/Plugin.dll input.bin leak

הסכום הצפוי הוא 10. במצב leak נשאר reference חזק גם אחרי Dispose, ולכן הסגירה הלוגית אינה מספיקה לפריקה. אחרי ClearRoot והיציאה מטווח החזקת ה־plugin ניתן להשלים את האיסוף. אם הבדיקה עדיין נכשלת, נבחן dump כדי למצוא root אחר ולא נחליף את הלולאה בהמתנה אינסופית.

בבדיקת מקטעים נשווה סכום של קלט אקראי עם seed קבוע לתוצאות פיצול בכל מיקום עבור קלט קצר, ובגדלים משתנים עבור קלט גדול. כדי לבדוק כשל באמצע נוסיף לרכיב בדיקה שזורקת אחרי סף מצטבר ונשתמש במעטפת pool שסופרת Rent ו־Return. ה־finally נשאר בעל אותו מבנה גם בגרסת הבדיקה. מדידת allocation בהרצה ראשונה כוללת פעילות חימום; המדידה המוצגת בקוד היא נקודת התחלה, ובדוח הביצועים נריץ סדרה מבוקרת אחרי חימום ונפריד טעינה מעיבוד.

ה־resolver כאן חולק חוזה מקומי מהימן לפי שם וגרסה. מוצר שמקבל binaries חיצוניים צריך גם מדיניות זהות, integrity ונתיבי טעינה שתואמים למודל האמון שלו. ההרחבה העצמאית יכולה להוסיף חוזה גרסה שנייה עם metadata של תוצאה, או להעביר רכיבים לתהליך נפרד. בכל הרחבה נשמור את ההבחנה בין עצירת עבודה, שחרור משאבים ופריקת קוד.