Skip to content

Commit

Permalink
Merge pull request #206 from SceneGate/feature/new-plugins
Browse files Browse the repository at this point in the history
❇️ Re-implementation of plugins API
  • Loading branch information
pleonex authored Nov 30, 2023
2 parents dc2ee45 + 04760de commit 7ae5d2c
Show file tree
Hide file tree
Showing 36 changed files with 1,772 additions and 1,267 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ featured binary IO and plugin support to support common formats. It's built in
- Table text replacements
- **Common encodings**: euc-jp, token-escaped encoding
- **API for simple encoding implementations**
- 🔌**Plugin** API to load and find types in .NET assemblies.

## Get started

Expand Down
File renamed without changes.
6 changes: 6 additions & 0 deletions docs/articles/core/formats/converters.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ method [`TDst Convert(TSrc)`](<xref:Yarhl.FileFormat.IConverter`2.Convert(`0)>).
This method creates a new object in the target type _converting_ the data from
the input.

```mermaid
flowchart LR
po(Po) --> converter["Binary2Po.Convert()\nIConverter#60;Po, BinaryFormat#62;"]
converter --> binary(Binary)
```

For instance the converter [`Po2Binary`](xref:Yarhl.Media.Text.Po2Binary)
implements `IConverter<Po, BinaryFormat>`. It allows to convert a
[`Po`](xref:Yarhl.Media.Text.Po) model format into a
Expand Down
6 changes: 5 additions & 1 deletion docs/articles/core/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,9 @@
href: ./binary/custom-streams.md

- name: 🔌 Plugins
- name: 🚧 Overview
- name: Overview
href: ../plugins/overview.md
- name: Loading assemblies
href: ../plugins/load-assembly.md
- name: Find converters
href: ../plugins/locate-types.md
89 changes: 89 additions & 0 deletions docs/articles/plugins/load-assembly.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Loading .NET assemblies

.NET provide already APIs to load additional assemblies via
[`AssemblyLoadContext`](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.loader.assemblyloadcontext).
Yarhl provides extensions methods for `AssemblyLoadContext` to facilitate
loading files from disk.

You can use the main `AssemblyLoadContext` from `AssemblyLoadContext.Default` to
load them. For advanced use cases, it's possible to create a new
`AssemblyLoadContext` that would provide isolation.

> [!TIP]
> If you plan to use [`ConverterLocator`](./locate-types.md#converterlocator),
> remember to call `ScanAssemblies` after loading new assemblies.
<!-- ignore markdown warning -->

> [!WARNING]
> Loading a .NET assembly may load also its required dependencies. You may run
> into dependency issues if they use different versions of a base library such
> as Yarhl or Newtonsoft.Json.
<!-- ignore markdown warning -->

> [!IMPORTANT]
> There may a security risk by loading **untrusted** assemblies from a file or a
> directory. .NET does provide any security feature to validate it's not
> malicious code.
## Load from file paths

The method
[`TryLoadFromAssemblyPath`](<xref:Yarhl.Plugins.AssemblyLoadContextExtensions.TryLoadFromAssemblyPath(System.Runtime.Loader.AssemblyLoadContext,System.String)>)
will try to load the .NET assembly in the given path. If this assembly fails to
load (e.g. it's not a .NET binary) it will return `null`.

Similar, the method
[`TryLoadFromAssembliesPath`](<xref:Yarhl.Plugins.AssemblyLoadContextExtensions.TryLoadFromAssembliesPath(System.Runtime.Loader.AssemblyLoadContext,System.Collections.Generic.IEnumerable{System.String})>)
will try to load every assembly in the list of paths given. If any of them fails
to load, no exception will be raised and it would be skipped.

Additionally, this API will skip any file where its name starts with any of the
following prefixes. The goal is to prevent loading unwanted dependencies. If you
want to force loading them, use `TryLoadFromAssemblyPath`.

- `System.`
- `Microsoft.`
- `netstandard`
- `nuget`
- `nunit`
- `testhost`

## Load from a directory

The method
[`TryLoadFromDirectory`](<xref:Yarhl.Plugins.AssemblyLoadContextExtensions.TryLoadFromDirectory(System.Runtime.Loader.AssemblyLoadContext,System.String,System.Boolean)>)
will try to load every file in the given directory with an extension `.dll` or
`.exe`. If any of them fails, no error will be reported and it would be skipped.

Via an argument it's possible to configure if it should load files from the
given directory or from its subdirectories recursively as well.

## Load from executing directory

A common use case it's to load every assembly from the executable directory.
Because .NET will load an assembly lazily, only when type actually need it, upon
startup not every assembly from the executable directory could be loaded.

The method
[`TryLoadFromBaseLoadDirectory`](<xref:Yarhl.Plugins.AssemblyLoadContextExtensions.TryLoadFromBaseLoadDirectory(System.Runtime.Loader.AssemblyLoadContext)>)
addresses this use case by loading every `.dll` and `.exe` from the current
`AppDomain.CurrentDomain.BaseDirectory`.

> [!TIP]
> To use _plugins_ in a _controlled way_, the application may add a set of
> `PackageReference`s. After running `dotnet publish` these dependencies will be
> copied to the output directory. At startup call
> `AssemblyLoadContext.Default.TryLoadFromBaseLoadDirectory` to load all of
> them. Otherwise, unless the application also references their types, the
> assemblies will not be loaded.
<!-- ignore warning -->

> [!NOTE]
> It does not use `Environment.ProcessPath` because sometimes the application
> (or tests) may run by passing the main library file to the `dotnet` host
> application (e.g. `dotnet MyApp.dll`). In that case it would scan the
> installation path of the .NET SDK instead of the application installation
> directory.
76 changes: 76 additions & 0 deletions docs/articles/plugins/locate-types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Locate types

After [loading external .NET assemblies](./load-assembly.md) containing
implementation of _formats_ and _converters_, the application can get a list of
them via `ConverterLocator`.

> [!NOTE]
> This is only needed if the application does not know in advance the converter
> to use. It can present the list to the user so it can choose. Or it can get
> the converter names from a configuration file and later find the actual type
> via reflection. For instance for generic Tinke-like applications.
## TypeLocator

The `TypeLocator` provides features to find types that implement or inherit a
given base type. It searches in the **loaded assemblies** of an
`AssemblyLoadContext` instance. The default _singleton_ instance is accesible
via `TypeLocator.Default` and it uses `AssemblyLoadContext.Default`. Normally
you don't need to create your own instance.

> [!NOTE]
> .NET loads assemblies lazily, when a code to run needs them. If you need a
> deterministic search consider loading every assembly from the application
> path. See
> [Load from executing directory](./load-assembly.md#load-from-executing-directory)
> for more information.
To find a list of types that inherit a given base class or implements an
interface use the method
[`FindImplementationsOf(Type)`](<xref:Yarhl.Plugins.TypeLocator.FindImplementationsOf(System.Type)>).
It searches for final types, that is: **classes that are public and not
abstract**. It returns information for each of these types in the _record_
[`TypeImplementationInfo`](xref:Yarhl.Plugins.TypeImplementationInfo)

For instance to find every _format_ in the loaded asssemblies use:

[!code-csharp[FindFormats](../../../src/Yarhl.Examples/Plugins/LocateTypesExamples.cs?name=FindFormats)]

The case of a _generic base type_ is special as types may implemented it
multiple. For instance a _class_ may implement `IConverter<Po, BinaryFormat>`
**and** `IConverter<BinaryFormat, Po>`. Using the _generic type definition_
(`typeof(IConverter<,>)`) to find types will throw an exception. Use this method
if you are searching for a specific implementation, like
`typeof(IConverter<Po, BinaryFormat>)`

Use the method
[`FindImplementationsOfGeneric(Type)`](<xref:Yarhl.Plugins.TypeLocator.FindImplementationsOfGeneric(System.Type)>)
to get a list of types implementing the **generic base type definition** with
any type arguments. For instance in the previous example calling
`FindImplementationsOfGeneric(typeof(IConverter<,>))` will return two results
for that class. One for `IConverter<Po, BinaryFormat>` and a second for
`IConverter<BinaryFormat, Po>`. The return type is the _record_
[`GenericTypeImplementationInfo`](xref:Yarhl.Plugins.GenericTypeImplementationInfo)

[!code-csharp[FindConverters](../../../src/Yarhl.Examples/Plugins/LocateTypesExamples.cs?name=FindConverters)]

## ConverterLocator

The [`ConverterLocator`](xref:Yarhl.Plugins.FileFormat.ConverterLocator) class
provides a cache of formats and converters found in the loaded assemblies.
During initialization (first use) it will use `TypeLocator` to find every format
and converter types. The `Default` singleton instance use `TypeLocator.Default`.
You can pass a custom `TypeLocator` via its public constructor.

The properties
[`Converters`](xref:Yarhl.Plugins.FileFormat.ConverterLocator.Converters) and
[`Formats`](xref:Yarhl.Plugins.FileFormat.ConverterLocator.Formats) provides a
list of the types found, so there is no need to re-scan the assemblies each
time.

> [!NOTE]
> If a new assembly is loaded in the `AssemblyLoadContext`, the
> `ConverterLocator` will need to performn a re-scan to find the new types. Make
> sure to call
> [`ConverterLocator.ScanAssemblies()`](xref:Yarhl.Plugins.FileFormat.ConverterLocator.ScanAssemblies)
> after loading new assemblies.
32 changes: 31 additions & 1 deletion docs/articles/plugins/overview.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,33 @@
# Plugins overview

TODO
`Yarhl.Plugins` provides a set of APIs that helps to load .NET assemblies and
find types.

Its main goal is to find [converter](../core/formats/converters.md) and
[format](../core/formats/formats.md) types in external .NET assemblies. Generic
applications, like [SceneGate](https://github.com/SceneGate/SceneGate), that
have no knowledge in the converters to use, could use the APIs to find and
propose them to the user.

The _plugins_ are regular .NET libraries or executable that contains
implementations of _converters_ and _formats_. They don't need to implement any
additional interface or fullfil other requirements.

The main APIs are:

- [`AssemblyLoadContextExtensions`](./load-assembly.md): extension methods for
`AssemblyLoadContext` to load .NET assemblies from disk.
- [`TypeLocator`](./locate-types.md#typelocator): find types that implement a
specific interface.
- [`ConverterLocator`](./locate-types.md#converterlocator): find _converter_ and
_format_ types.

```mermaid
flowchart TB
Application ---> |Load external .NET assemblies| AssemblyLoadContext
Application --> |Find converters| ConverterLocator
ConverterLocator --> |Find implementations\nof IConverter<,>| TypeLocator
TypeLocator --> |Iterate through types in\nloaded assemblies| AssemblyLoadContext
```

You can get more information in their subpage.
3 changes: 2 additions & 1 deletion docs/docfx.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
{
"files": [
"Yarhl/*.csproj",
"Yarhl.Media.Text/*.csproj"
"Yarhl.Media.Text/*.csproj",
"Yarhl.Plugins/*.csproj",
],
"src": "../src"
}
Expand Down
5 changes: 4 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ formats** It empowers you with...
serialization.
- 📃 ... **standard formats** implementation like **PO** for translations.
- 📂 ... virtual **file system** to unpack and pack containers efficiently.
- 🔌... **plugin** API to find formats and converters in .NET assemblies.

## Usage

Expand All @@ -25,7 +26,9 @@ libraries only support .NET LTS versions: **.NET 6.0** and **.NET 8.0**.
- `Yarhl.Media.Text`: translation formats and converters (Po), table replacer.
- `Yarhl.Media.Text.Encoding`: _euc-jp_ and token-escaped encodings.
- [![Yarhl.Plugins](https://img.shields.io/nuget/v/Yarhl.Plugins?label=Yarhl.Plugins&logo=nuget)](https://www.nuget.org/packages/Yarhl.Plugins)
- `Yarhl.Plugins`: discover formats and converters from .NET assemblies.
- `Yarhl.Plugins`: load nearby .NET assemblies and find type implementations.
- `Yarhl.Plugins.FileFormat`: find formats and converters from loaded
assemblies.

> [!NOTE]
> _Are you planning to try a preview version?_
Expand Down
8 changes: 4 additions & 4 deletions src/Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
<Project>
<!-- Centralize dependency management -->
<ItemGroup>
<PackageVersion Include="FluentAssertions" Version="6.12.0" />
<PackageVersion Include="System.Composition" Version="6.0.0" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="6.0.0" />
<PackageVersion Include="System.Runtime.Loader" Version="4.3.0" />
<PackageVersion Include="Microsoft.IO.RecyclableMemoryStream" Version="2.2.0" />
<PackageVersion Include="System.Buffers" Version="4.5.1" />

<PackageVersion Include="FluentAssertions" Version="6.12.0" />
<PackageVersion Include="NUnit" Version="3.14.0" />
<PackageVersion Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageVersion Include="coverlet.collector" Version="6.0.0" />
<PackageVersion Include="Moq" Version="4.20.69" />

<PackageVersion Include="BenchmarkDotNet" Version="0.13.1" />
<PackageVersion Include="System.CommandLine" Version="2.0.0-beta3.22114.1" />

<PackageVersion Include="SonarAnalyzer.CSharp" Version="9.12.0.78982" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.507" />
</ItemGroup>
Expand Down
52 changes: 52 additions & 0 deletions src/Yarhl.Examples/Plugins/LocateTypesExamples.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright (c) 2023 SceneGate

// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:

// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
namespace Yarhl.Examples.Plugins;

using Yarhl.FileFormat;
using Yarhl.Plugins;

public static class LocateTypesExamples
{
public static void FindFormats()
{
#region FindFormats
TypeImplementationInfo[] formatsInfo = TypeLocator.Default
.FindImplementationsOf(typeof(IFormat))
.ToArray();

Console.WriteLine(formatsInfo[0].Name); // e.g. Yarhl.IO.BinaryFormat
Console.WriteLine(formatsInfo[0].Type); // e.g. Type object for BinaryFormat
#endregion
}

public static void FindConverters()
{
#region FindConverters
GenericTypeImplementationInfo[] convertersInfo = TypeLocator.Default
.FindImplementationsOfGeneric(typeof(IConverter<,>))
.ToArray();

Console.WriteLine(convertersInfo[0].Name); // e.g. Yarhl.Media.Text.Binary2Po
Console.WriteLine(convertersInfo[0].Type); // e.g. Type object for Yarhl.Media.Text.Binary2Po
Console.WriteLine(convertersInfo[0].GenericBaseType); // e.g. Type IConverter<BinaryFormat, Po>
Console.WriteLine(convertersInfo[0].GenericTypeParameters); // e.g. [BinaryFormat, Po]
#endregion
}
}
1 change: 1 addition & 0 deletions src/Yarhl.Examples/Yarhl.Examples.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<ItemGroup>
<ProjectReference Include="..\Yarhl\Yarhl.csproj" />
<ProjectReference Include="..\Yarhl.Media.Text\Yarhl.Media.Text.csproj" />
<ProjectReference Include="..\Yarhl.Plugins\Yarhl.Plugins.csproj" />
</ItemGroup>

</Project>
Loading

0 comments on commit 7ae5d2c

Please sign in to comment.