Skip to content

Instantly share code, notes, and snippets.

@Owen1212055
Created July 16, 2022 03:53
Show Gist options
  • Save Owen1212055/f5da742f79f06545df92b542f92edb92 to your computer and use it in GitHub Desktop.
Save Owen1212055/f5da742f79f06545df92b542f92edb92 to your computer and use it in GitHub Desktop.
Paper Plugin Implementation (as of 7/15/22)

Implementation

Loading

When starting the server:

  • PaperClassloaderBytecodeModifier (server) is initialized, this is set in the ClassloaderBytecodeModifier (api) singleton class.
  • Providers are created and loaded by a ProviderSource.
Providers provider = Providers.INSTANCE;
provider.addProviders(DirectoryProviderSource.INSTANCE, pluginfile.toPath());

Current provider sources:

DirectoryProviderSource (context: a directory path), FileProviderSource (context: a path), PluginFlagProviderSource (context, a list of files) Provider sources are given a context parameter, where it is responsible for loading providers depending on the context.

public class Providers {

    private static final Logger LOGGER = Logger.getLogger("PluginLoading"); // Name for easier understanding
    public static final Providers INSTANCE = new Providers();

    public <C> void addProviders(ProviderSource<C> source, C context) {
        try {
            source.registerProviders(ServerEntryPointHandler.INSTANCE, context);
        } catch (Throwable e) {
            LOGGER.log(Level.SEVERE, e.getMessage(), e);
        }
    }

}

So in this case, the DirectoryProviderSource is given the plugin directory as context to load all providers in that directory.

EntrypointHandler

These provider sources then load providers into EntrypointHandlers.

public interface EntrypointHandler {

    void enter(Entrypoint<?> entrypoint);

    <T extends PluginProvider<?>> void register(Entrypoint<T> entrypoint, T provider);
}

The current entrypoints are BOOTSTRAPPER and PLUGIN.

The reason for needing this bootstrap is because of dynamic plugin loading because of stuff like the api.

 RuntimePluginEntrypointHandler runtimePluginEntrypointHandler = new RuntimePluginEntrypointHandler(this);
        try {
            FILE_PROVIDER_SOURCE.registerProviders(runtimePluginEntrypointHandler, file.toPath());

            runtimePluginEntrypointHandler.enter(Entrypoint.PLUGIN);
        } catch (SerializationException |
                 InvalidDescriptionException ex) { // The spigot implementation wraps it in an invalid plugin exception
            throw new InvalidPluginException(ex);
        } catch (Throwable e) {
            throw new InvalidPluginException(e);
        }

This allows us to capture stored providers via the entry handler. The special thing about RuntimePluginEntrypointHandler is it does not allow BOOTSTRAP entrypoint, this is to prevent the loading of paper plugins with initilizers... since we can't load those during runtime.

For the implementation of ServerEntryPointHandler registering these entry points will be put into ProviderStorage.

public class ServerEntryPointHandler implements EntrypointHandler {

    public static final ServerEntryPointHandler INSTANCE = new ServerEntryPointHandler();
    
    ...
    
    public ServerEntryPointHandler() {
        this.storage.put(Entrypoint.BOOTSTRAPPER, new BootstrapProviderStorage());
        this.storage.put(Entrypoint.PLUGIN, new ServerPluginProviderStorage());
    }
    
     @Override
    public void enter(Entrypoint<?> entrypoint) {
        ProviderStorage<?> storage = this.storage.get(entrypoint);
        ...
        storage.enter();
    }

    @Override
    public <T extends PluginProvider<?>> void register(Entrypoint<T> entrypoint, T provider) {
        ProviderStorage<T> providerStorage = this.get(entrypoint);
        ...
        providerStorage.register(provider);
    }
ProviderStorage
public interface ProviderStorage<T extends PluginProvider<?>> {

    void register(T provider);

    void enter();

    Iterable<T> getRegisteredProviders();

}

Provider storage is meant to store multiple providers of the same type together. This is important when for example providers depend on eachother and must load in a certain order. Provider storage is typically responsible for creating the JavaPlugins/PluginBootstrappers and properly registering/adding them.

The ServerPluginProviderStorage has two options for loading plugins, ModernPluginLoadingStrategy/LegacyPluginLoadingStrategy. These are how they handle cyclic dependencies and stuff. But generally, load plugin providers depending on their dependencies.

But, going back to ServerEntryPointHandler... in many places the singleton will be called and the entrypoint will be executed. This will cause all providers in the desired plugin provider storage to be loaded.

io.papermc.paper.plugin.provider.service.entrypoint.ServerEntryPointHandler.INSTANCE.enter(io.papermc.paper.plugin.provider.service.entrypoint.Entrypoint.BOOTSTRAPPER);

Providers

Providers are only currently created by the FileProviderSource, where it tries to find a PluginFileType. These plugin file types are currently: PAPER, SPIGOT. These are identified but what file is first found (paper-plugin.yml, then plugin.yml)

public static final PluginFileType<SpigotPluginProvider> SPIGOT = new PluginFileType<>("plugin.yml", SpigotPluginProvider.FACTORY) {
        @Override
        protected void register(EntrypointHandler entrypointHandler, SpigotPluginProvider provider) {
            entrypointHandler.register(Entrypoint.PLUGIN, provider);
        }
    };

For example, this is the spigot file type that is loaded into the PLUGIN entrypoint.

More complicated providers, like the paper provider, registers a bit more...

public static final PluginFileType<PaperParentProvider> PAPER = new PluginFileType<>("paper-plugin.yml", PaperParentProvider.FACTORY) {
        @Override
        protected void register(EntrypointHandler entrypointHandler, PaperParentProvider provider) {
            PaperPluginProvider parent = provider.createInstance();
            if (parent.shouldCreateBootstrap()) {
                PaperPluginProvider.PaperBootstrapProvider bootstrapPluginProvider = parent.createBootstrapProvider();

                entrypointHandler.register(Entrypoint.BOOTSTRAPPER, bootstrapPluginProvider);
                entrypointHandler.register(Entrypoint.PLUGIN, parent.createPluginProvider(bootstrapPluginProvider));
            } else {
                entrypointHandler.register(Entrypoint.PLUGIN, parent.createPluginProvider());
            }
        }
    };

So, there are currently two types of main providers: PaperParentProvider and SpigotPluginProvider.

SpigotPluginProvider

These are very simple, and simply provide a JavaPlugin.

PaperParentProvider

This represents a complicated sharing process between multiple providers. A PaperParentProvider provides a PaperPluginProvider (new name? appreciated).

@Override
    public PaperPluginProvider createInstance() {
        PaperClasspathBuilder builder = new PaperClasspathBuilder();

        if (this.description.getLoader() != null) {
            try (
                PaperSimplePluginClassLoader simplePluginClassLoader = new PaperSimplePluginClassLoader(this.path, this.jarFile, this.description, this.getClass().getClassLoader())
            ) {
                PluginLoader loader = ProviderUtil.loadClass(this.description.getLoader(), PluginLoader.class, simplePluginClassLoader);
                loader.classloader(builder);

            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

        return new PaperPluginProvider(this.path, this.jarFile, this.description, builder.buildClassLoader(this), this.logger);
    }

A PaperParentProvider is responsible for creating the inital classpath that the plugin will use. After, a PaperPluginProvider is created. Now, this by itself is not a provider but holds to inner classes: PaperBootstrapProvider & PaperServerPluginProvider. So although they are two separate providers, they both share a parent PaperPluginProvider which allows them to share a classloader, configuration, file, etc.

Now, going back to how they are stored.

After this, plugins are loaded! Yay! Note I am still working on making the implementation of SimplePluginManager cleaner... I have to hack it on and ugh, it's a mess.

Other stuff:

PluginConfiguration

This is now a middle class between the paper plugin configuration and the bukkit plugin description. Paper plugin configs are created using configurate.

Paper config exclusives contain:

  • loader
  • bootstrapper
  • plugin command permission messages support minimessage

Bukkit plugin config exclusives contain:

  • classLoaderOf: unused, not implemented in paper config
  • awareness: unused, not implemented in paper config
  • libraries: it is encouraged to use the runtime plugin loader instead for paper
ClassLoader stuff

Currently spigot stores the main classloader storage in the JavaPluginLoader. Paper eliminates that and is moving all that logic away from that class. There is now a PaperPluginClassLoaderStorage singleton, which holds ConfiguredPluginClassLoaders (both bukkit & paper classloader classes implement this)

@ApiStatus.Internal
public interface ConfiguredPluginClassLoader {
    PluginConfiguration getConfiguration();

    Class<?> loadClass(@NotNull String name, boolean resolve, boolean checkGlobal, boolean checkLibraries) throws ClassNotFoundException;

    // Called in the constructor of javaplugin, at the very top
    void init(JavaPlugin plugin);
}

The implementation of PaperPluginClassLoaderStorage is pretty much identical to what the logic was in JavaPluginLoader, it's just moved to a nicer place and allows for multiple different classloaders (Paper Classloader is not in api).

Current paper classloader implementations are PaperSimplePluginClassLoader -> PaperPluginClassLoader (ConfiguredPluginClassLoader). The PaperSimplePluginClassLoader is the simplest paper classloader, and is currently used in the PaperLoader (this has an isolated classloader). This is basically a URLClassLoader that supports the paper ClassloaderBytecodeModifier. PaperPluginClassLoader then also has a libraryLoader and PaperPluginClassLoaderStorage. This allows it to refer to other classloaders (plugins) and resolve their own libraries resolved in their PluginLoader.

Plugin Loader
public class TestPluginLoader implements PluginLoader {
    
    @Override
    public void classloader(PluginClasspathBuilder classpathBuilder) {
        classpathBuilder.addLibrary(new JarLibrary(Path.of("bob.jar")));

        MavenLibraryResolver resolver = new MavenLibraryResolver();
        resolver.addDependency(new Dependency(new DefaultArtifact("com.owen1212055:particlehelper:1.0.0-SNAPSHOT"), null));
        resolver.addRepository(new RemoteRepository.Builder("bytecode", "default", "https://repo.bytecode.space/repository/maven-public/").build());

        resolver.setFilter(new DependencyFilter() {
            @Override
            public boolean accept(DependencyNode node, List<DependencyNode> parents) {
                return true;
            }
        });

        classpathBuilder.addLibrary(resolver);
    }
}

Plugin loaders are mostly all implemented on the api side, where ClassPathLibrarys, for example the MavenLibraryResolver and JarLibrary are meant to register jars into a LibraryStore.``

public interface LibraryStore {

    void addLibrary(@NotNull Path library);

}

The PluginClasspathBuilder only supports adding ClassPathLibraries into themselves.

public PaperPluginClassLoader buildClassLoader(PluginProvider<?> provider) {
        PaperLibraryStore paperLibraryStore = new PaperLibraryStore();
        for (ClassPathLibrary library : this.libraries) {
            library.addToLibraryStore(paperLibraryStore);
        }

        List<Path> paths = paperLibraryStore.getPaths();
        URL[] urls = new URL[paths.size()];
        for (int i = 0; i < paths.size(); i++) {
            Path path = paperLibraryStore.getPaths().get(i);
            try {
                urls[i] = path.toUri().toURL();
            } catch (MalformedURLException e) {
                throw new AssertionError(e);
            }
        }

        try {
            return new PaperPluginClassLoader(provider.getLogger(), provider.getSource(), provider.file(), provider.getConfiguration(), this.getClass().getClassLoader(), new URLClassLoader(urls));
        } catch (IOException exception) {
            throw new RuntimeException(exception);
        }
    }

We then build a paper plugin classloader using these libraries by creating our own paper library store (server) and grabbing all the URLS of the jars.

Plugin Bootstrap

The plugin bootstrap currently supports supplying your own custom java plugin (must be the same as defined in paper-plugin.yml:main... not sure if I will keep this restriction)

public interface PluginBootstrap {

   
    void boostrap(@NotNull PluginBootstrapContext context);

    @NotNull
    default JavaPlugin createPlugin(@NotNull PluginBootstrapContext context) {
        return ProviderUtil.loadClass(context.getConfiguration().getMain(), JavaPlugin.class, this.getClass().getClassLoader());
    }
}

This allows you to pass context into your javaplugin constructor from your plugin bootstrap class.

Each method provides a context object.

@ApiStatus.NonExtendable
public interface PluginBootstrapContext {

    @NotNull
    PluginConfiguration getConfiguration();

   
    @NotNull
    Path getDataDirectory();

    
    @NotNull
    Path getConfigurationFile();

 
    @NotNull
    Logger getLogger();
}

This allows you to resolve things like config/data info early, which is important inorder to register custom things via the bootstrap method.

Api Exposure

Api exposure is currently very low, I try to keep it so that there is very little of this internal implementation shown... because that is what makes the current plugin system so hard to change. image The classes in yellow are marked as internal. As you can see, most of this is internal...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment