Как написать плагин для rust

Rust Plugins

This is a guide for setting your Rust application up with Rust plugins that can be loaded dynamically at runtime. Additionally, this plugin setup allows plugins to make calls to the application’s public API so that it can make use of the same data structures and utilities for extending the application.

Warning: After further testing, I cannot confirm that this plugin setup will work for all applications that have other crates as dependencies. It seems to work fine with the steps outlined in this tutorial, but I was not able to get it to work with a large project like Amethyst.

Additionally, this will only allow you to create plugins using the same version of Rust that the application was built with. Unfortunately, these issues defeated my use-case, but the tutorial may still be useful for one reason or another so I leave it here for reference.

If you are wanting to attempt something similar, I recommend looking at the ABI Stable Crates project.

This is a quick, somewhat unpolished tutorial that I figured I would make as I explored the subject for the first time myself.

The specific purpose for the plugins, in my case, is to allow for a Rust game to be extended/modded by writing Rust. I want the plugins to have full access to the game’s API so that plugins are just about as powerful as making changes to the game core, without you having to manually create bindings to get access to engine internals.

The full source code for the final version of this tutorial can be found here.

Note: This guide assumes that you are on Linux, and has not been tested on Windows, or Mac, but, conceptually, everything should work the same except for the extensions for shared libraries being different on the different platforms ( .so on Linux, .dll on Windows, .dylib on Mac ).

Create The App

The first thing we need is a place to put our crates, like a rust-plugins folder. It is here that we will put our app crate and our plugin crate.

Now lets create the app directory inside the one we created for our project:

Now move to the new app directory edit the src/main.rs file to look like so:

use app;

fn main() {
    app::run();
}

This will simply execute our app’s run method. We want to keep the main function very simple. All of the application functionality will be put into the app’s library. In that light, we need to create our crate’s src/lib.rs file:

pub fn run() {
    println!("Starting App");
}

If you run cargo run now to run your app, you should get «Starting App» printed to the terminal. Now lets spend a little bit of time to understand what has happened. You’ve probably done this before, but in order to understand how plugins will work, we have to understand more about how different libraries and portions of our app end up a runnable program.

If we look in our target/debug directory, we can see the artifacts that were built when we ran cargo run.

> ls target/debug/
app  app.d  build/  deps/  examples/  incremental/  libapp.d  libapp.rlib  native/

In there we can see our program, app, which can be run manually:

> ./target/debug/app
Starting App

Rust has packed everything that your app needs to run inside of that one executable. If you copy that binary to any other system, it will run without needing any other libraries. Also the size of the binary is 1.7M.

The way that rust builds applications by default is great for most situations, and it lets you easily distribute your app just by providing a single binary, but for our use, we want to allow dynamically loading portions of the app that may not have come with it, and this requires some changes.

By default Rust will link all application dependencies statically into the the final executable. In this case, our app only depends on the standard library, which it uses to print to standard out. The problem with static linking is that only the app that is link to a static library can actually use the library. This means that if we have plugins, our plugins will not be able to call any of the functions in our application’s library. For this tutorial we do want our plugins to be able to call our application’s API to make use of utilities and functionality provided in our app. This requires dynamic linking.

To make rust create a dynamic library for our app that our plugins can link to, we first need to tell Cargo to compile the app as a dynamic library by adding this to the Cargo.toml file:

[lib]
crate-type = ["dylib", "rlib"]

In the above config we tell cargo to compile a dylib or dynamic library and an rlib or rust library. The dylib will be a .so shared library that will contain the machine code for our app’s library and is needed when running the app and plugins. The rlib will make a .rlib file that provides rust with extra metadata that allows it to link plugins to the app’s library at compile time. Without the rlib build of the library, our plugins would not be able to tell which functions are defined in the library without us providing the entire source-code for the app. The rlib is almost like a kind of header file that gives rust the info it needs to link to the crate without needing source code ( I think ).

Note: There is another crate type called cdylib that can be used instead of dylib, but it behaves somewhat differently. It may be a better solution as it is not dependent on the Rust compiler version being exactly the same for the app and the plugins. I am trying to understand the full differences and have opened up a forum topic on the Rust user forum to discuss it. My current understanding can be found in Appendix A.

Additionally we need to tell cargo to add some flags to the its rust compiler calls. These settings go in a .cargo/config file:

[build]
rustflags = ["-C", "prefer-dynamic", "-C", "rpath"]

prefer-dynamic tells the compiler to prefer dynamic linking when compiling the app. This means that instead of statically linking the standard library into the our app, it will leave the standard library as a separate dynamically linked shared library ( .so ) file. This means that both our app and our plugins will be able to link to the same standard library, without duplicating the standard library for each plugin.

rpath tells the compiler to make the binary for our app look in the directory that it is in for shared libraries. This means that we can put the shared libraries that our app needs, such as the rust standard library, in the same director as the app binary and not require that the user add the libraries to the system PATH or the LD_LIBRARY_PATH.

If we run cargo run now, our app should still run the same, but things are a bit different under the hood.

For one, if we look in the target/debug directory now, we should see a libapp.so file in it which is about 14 kilobytes. Also, instead of our app binary being almost 2 megabytes, it is only 19 kilobytes. So, what happened? Well, instead of bundling everything up into our one binary, Rust has now compiled each library to its own dynamic library file ( the .so file, or .dll on Windows ) and dynamically linked our app binary to those libraries.

Dynamic linking means that, when you run the program, it will look around on your system for the libraries that it needs, because the libraries are not built into it. The places that the system will look for the dynamic libraries depends on the system. On Linux it will look in places like /usr/lib and also in any places indicated by the LD_LIBRARY_PATH. On Windows it will look in your PATH.

If you try to run the app manually, now you will actually get an error:

> ./target/debug/app
./target/debug/app: error while loading shared libraries: libstd-8e7d7d74c91e7cfe.so: cannot open shared object file: No such file or directory

This is because we have not put the Rust standard library somewhere that our app can find it! Because we added the rpath rust flag in our cargo config earlier, our app will look in the directory that it is in for dynamic libraries, as well as in the system search paths. The rust libstd-*.so file isn’t in a system directory or in the executable’s directory, so it throws an error saying that it cannot be found. All we have to do is copy that library to the our target/debug folder to get the app to run. If you are using rustup, you can find the libstd library in your rustup dir ( I’m using nightly rust, but make sure you choose whatever toolchain you compiled the app with ):

> cp ~/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/libstd-8e7d7d74c91e7cfe.so target/debug/
> ./target/debug/app
Starting App

Now that our app can find the libraries it needs, it runs successfully! Also, the libstd file explains where the rest of the file size went when we switched to dynamic linking. The libstd library is 5M, which is larger than our executable was when it was statically linked, but that is probably because, when statically linking, rust can remove portions of the library that is not used, but when dynamically linking, you never know what portions of the library an app might use, so you have to make sure that it is all there all of the time.

Dynamic linking can be less convenient for distribution because you need more files with your app but it allows multiple applications to share the same libraries, which can save on disc space if there are many binaries or plugins that are using the same library. This helps us for our plugins use-case because all of the plugins will share the same app library and standard library.

Create an App Library Function

Before we move on to creating our plugin, lets create a function that our plugin can call and put it in our app library:

src/lib.rs:

pub fn test_app_func(message: &str) {
    println!("test_app_func("{}")", message);
}

Testing that our plugin can call this function in our app’s library will prove that our plugin can, in fact, make use of our app’s public rust API.

Create a Plugin

The next thing we are going to do is create our plugin crate. Go ahead and cd back to your project folder and create the plugin crate alongside the app crate and the move to the plugin dir.

cargo new --lib plugin1
cd plugin1

For this crate we are going to make similar Cargo.toml and .config/cargo changes that we make for our app to make it dynamically link all of its dependencies. The only difference in this case is that we don’t need need to set the crate-type to include rlib in the Cargo.toml file. Instead we set it to dylib only:

Cargo.toml:

[lib]
crate-type = ["dylib"]

.cargo/config:

[build]
rustflags = ["-C", "prefer-dynamic", "-C", "rpath"]

The reason the rlib build is not needed for plugins is because we don’t plan on linking any other rust libraries to the plugin crate. The rlib build is only used when linking other rust libraries/binaries to this one. Granted, if you wanted to let your plugin have plugins, you would still want to build the rlib, but we’re not going to take this that far here.

After that, we will add a run() function that will be called by our app to execute the plugin’s functionality. Eventually plugins will be able to do more than just run but for now that is all we will do with it.

src/lib.rs:

extern crate app;

#[no_mangle]
pub fn run() {
    println!("Running plugin1");
    app::test_app_func("Hello from plugin 1");
}

Notice that we specify app as an external crate; if we had added app as a Cargo dependency, we could have done use app; instead. Our run function is simple and just prints some output before calling the test_app_func that we created in our app library. The #[no_mangle] attribute on the run() function tells the compiler not to add any extra metadata to that symbol in the compiled output, this allows us to call the function by name when we later load it into our app dynamically.

Attempting to cargo build the crate right now will tell us that it can’t find the app crate. This is because we didn’t add it as a dependency to our Cargo.toml file. Now, if we added the app crate to the plugin’s dependencies, it would be able to compile, but it would also re-compile the app library, when we already have the app compiled. There is no reason to compile the app library twice, especially if it is a big app, so, instead, lets add the app library to our plugin’s search path so that it will find our already built app crate.

To tell cargo how to find our app crate, we create a build.rs script. The build.rs script can be used to do any kind of setup necessary to compile a library. In our case we just need to feed cargo some specially understood flags that tell it where to find our pre-compiled app library.

build.rs:

fn main() {
    // Add our app's build directory to the lib search path.
    println!("cargo:rustc-link-search=../app/target/debug");
    // Add the app's dependency directory to the lib search path.
    // This is may be required if the app depends on any external "derive"
    // crates like the `dlopen_derive` crate that we add later.
    println!("cargo:rustc-link-search=../app/target/debug/deps");
    // Link to the `app` crate library. This tells cargo to actually link
    // to the `app` crate that we include using `extern crate app;`.
    println!("cargo:rustc-link-lib=app");
}

Now we can run cargo build and we will get a new libplugin1.so file in our target/debug ( if it fails see note below ). As we intended, the plugin only contains the code that is in the plugin and weighs only 14 kilobytes. Yay, we have successfully built a plugin! Lets go over what happened when we built it.

note: If you run cargo build and get an error like error[E0464]: multiple matching crates for 'app', change directory to your app directory and run cargo clean followed by cargo build. This will get rid of any extra rlib file that may have been left over from when we first built our app as a standalone binary. After doing that you should be able to come back to your plugin and successfully run cargo build to build the library.

When we run cargo build, cargo will first run our build.rs script and read the standard output of that script to look for cargo directives. In this case, our script tells cargo to look in the debug build dir of our app for libraries and to link to the app library. When compiling our rust library, the compiler will read our app’s libapp.rlib which contains all of the metadata needed to compile rust code that talks to that library, similar to C/C++ header files. After the rust code is compiled, it will call the system linker to link our plugin library, libplugin1.so, to libapp.so so that it can call functions defined in our app library.

Now that we have an app and a plugin, we need to make our app load the plugin!

Loading a Plugin

Now we are ready to actually do some awesome stuff, loading the plugin into our app. To load the plugin we are going to use the dlopen crate. The dlopen crate will do the actual loading of the shared libraries and takes care of the lower level stuff so we don’t have to. Our first step, then, is to add that crate to the Cargo.toml for our app.

[dependencies]
dlopen = "0.1.6"
dlopen_derive = "0.1.3"

Then update your app’s src/lib.rs file to look like this:

#[macro_use]
extern crate dlopen_derive;
use dlopen::wrapper::{Container, WrapperApi};

#[derive(WrapperApi)]
struct PluginApi {
    run: extern fn(),
}

pub fn run() {
    println!("Starting App");

    let plugin_api_wrapper: Container<PluginApi> = unsafe { Container::load("plugins/libplugin1.so") }.unwrap();
    plugin_api_wrapper.run();
}

pub fn test_app_func(message: &str) {
    println!("test_app_func("{}")", message);
}

There is a little bit going on here, but it is still fairly simple, thanks to the dlopen library. We create a PluginApi struct that represents the functions that we can call in loaded plugins. We use dlopen to load our plugin shared library, and store it in plugin_api_wrapper. We can then call the run() function, and it will execute the run() function in our plugin. The run() function in our plugin should then call test_app_func with a message that should be printed to the console.

Before we run it, lets create a plugins directory in our app crate directory and copy our libplugin1.so file into it from our plugin’s build directory. After that, go ahead and test it with cargo run:

> cd app
> mkdir plugins
> cp ../plugin1/target/debug/libplugin1.so plugins
> cargo run
   Compiling app v0.1.0 (/home/zicklag/rust/test-lab/plugins/rust-plugins-test2/project-tutorial/app)
   ...
    Finished dev [unoptimized + debuginfo] target(s) in 1.67s
     Running `target/debug/app`
Starting App
Running plugin1
test_app_func("Hello from plugin 1")

It works! You should also be able to run it manually, but you will have to re-copy the libstd library back into the build directory because we ran a cargo clean earlier:

> cp ~/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/libstd-8e7d7d74c91e7cfe.so target/debug/
> ./target/debug/app
Starting App
Running plugin1
test_app_func("Hello from plugin 1")

Notice that Rust bundled the new dependencies of our app, such as the dlopen crate, into our libapp.so; it is now 534 kilobytes instead of the original 14 kilobytes. Apparently, even though it dynamically links libstd, it decided to statically link the dlopen crate to libapp. This is fine and is nice because we don’t need to have a shared library for every crate dependency. If we wanted to expose one of the crates that our app depends on to our plugins, we could do that simply by re-exporting the library in our app library ( this is yet to be tested ).

Notes So Far

In this example, we added the path to our app library to our plugin’s build script so that we could compile the plugin with a link to our app. In a more organized situation, as the designer of the app, you would probably provide the shared libraries and the rlibs that are required to link to the app for your users, so that they could build their plugins against those, without having to have the source code and compile the app themselves.

What we are going to focus on next is making our plugin API more powerful so that the app has a way to find out more about the plugin, instead of just having a run function.

Improving the Plugin API ( And Our App )

Now that we have basics of plugin loading, lets make our app do something. We’re going to setup a simple app that will infinitely prompt for a command, and respond to the user’s input. The only command that comes with the app will be the exit command that lets the user exit the program. Otherwise, all other commands will be provided by plugins.

Let’s get that loop going without plugins first:

src/lib.rs:

// ...
pub fn run() {
    println!("Starting App");

    // Comment this out for now
    // let plugin_api_wrapper: Container<PluginApi> = unsafe { Container::load("plugins/libplugin1.so") }.unwrap();
    // plugin_api_wrapper.run();

    loop {
        // Prompt
        println!("Enter command:");

        // Read input
        let mut message = String::new();
        std::io::stdin().read_line(&mut message).unwrap();

        // Trim newline
        message = message.trim().into();

        // Check command
        if message == "exit" {
            break
        }
    }
}
// ...

Now we can cargo run our app and get our own little command prompt.

Now we want to refine our plugin API a bit. Instead of using a run function to execute our plugins, we are going to use an get_plugin function which is expected to return a pointer to a struct that implements a Plugin trait. The Plugin trait will require that each plugin implement the handle_command() function so that it can handle commands pass to the user.

Here is the full updated app/src/lib.rs:

#[macro_use]
extern crate dlopen_derive;
use dlopen::wrapper::{Container, WrapperApi};

// The trait that must be implemented by plugins to allow them to handle
// commands.
pub trait Plugin {
    fn handle_command(&self, command: &str);
}

#[derive(WrapperApi)]
struct PluginApi {
    // The plugin library must implement this function and return a raw pointer
    // to a Plugin struct.
    get_plugin: extern fn() -> *mut Plugin,
}

pub fn run() {
    println!("Starting App");

    // Load the plugin by name from the plugins directory
    let plugin_api_wrapper: Container<PluginApi> = unsafe { Container::load("plugins/libplugin1.so") }.unwrap();
    let plugin = unsafe { Box::from_raw(plugin_api_wrapper.get_plugin()) };

    loop {
        // Prompt
        println!("Enter command:");

        // Read input
        let mut message = String::new();
        std::io::stdin().read_line(&mut message).unwrap();

        // Trim newline
        message = message.trim().into();

        // Give the plugin a chance to handle the command
        plugin.handle_command(&message);

        // Check command
        if message == "exit" {
            break
        }
    }
}

And our updated plugin1/src/lib.rs:

extern crate app;

use app::Plugin;

// Our plugin implementation
struct Plugin1;

impl Plugin for Plugin1 {
    fn handle_command(&self, command: &str) {
        // Handle the `plugin1` command
        if command == "plugin1" {
            println!("Hey you triggered my 'plugin1' command!");

        // Handle an `echo` command
        } else if command.starts_with("echo ") {
            println!("Echo-ing what you said: {}", command);
        }
    }
}

#[no_mangle]
pub fn get_plugin() -> *mut Plugin {
    println!("Running plugin1");

    // Return a raw pointer to an instance of our plugin
    Box::into_raw(Box::new(Plugin1 {}))
}

Now we can:

  • rebuild our app
  • rebuild our plugin
  • copy the newly built libplugin1.so into our app’s plugins/ directory, and
  • run our app to get our mini command prompt

Here is an example of the result:

Starting App
Running plugin1
Enter command:
plugin1
Hey you triggered my 'plugin1' command!
Enter command:
echo hello world
Echo-ing what you said: echo hello world
Enter command:
exit

We used our plugin to provide custom commands to our command prompt!

This is as far as this tutorial will take you and there is obviously a lot that could be improved. For one, you probably don’t want to be loading plugins by name and you are going to want to be able to have more than one. All of that is simple to implement on top of the base that we have worked on here and I leave it up to the reader to explore how to do that if they so desire.

Closing Thoughts

This is actually the first time that I have done any of this, so I’m still getting to understand how everything fits together, but hopefully this presents a good picture of how you can setup plugins in Rust.

Many thanks to @Michael-F-Bryan for the plugin section of his Rust FFI Guide. I wouldn’t have figure out how to do this without that. I may have missed something or given incorrect instructions somewhere in the tutorial so open an issue if you have any problems with it. 😃

Imagine you are implementing a calculator application and want users to be able
to extend the application with their own functionality. For example, imagine a
user wants to provide a random() function that generates true random numbers
using random.org instead of the pseudo-random numbers that a crate like
rand would provide.

The Rust language gives you a lot of really powerful tools for adding
flexibility and extensibility to your applications (e.g. traits, enums,
macros), but all of these happen at compile time. Unfortunately, to get the
flexibility that we’re looking we’ll need to be able to add new functionalty at
runtime.

This can be achieved using a technique called Dynamic Loading.

The code written in this article is available on GitHub. Feel free to
browse through and steal code or inspiration.

If you found this useful or spotted a bug, let me know on the blog’s
issue tracker!

What Is Dynamic Loading?

Link to heading

Dynamic loading is a mechanism provided by all mainstream Operating Systems
where a library can be loaded at runtime so the user can retrieve addresses of
functions or variables. The address of these functions and variables can then
be used just like any other pointer.

On *nix platforms, the dlopen() function is used to load a library into memory
and dlsym() lets you get a pointer to something via its symbol name. Something
to remember is that symbols don’t contain any type information so the caller
has to (unsafe-ly) cast the pointer to the right type.

This is normally done by having some sort of contract with the library being
loaded ahead of time (e.g. a header file declares the "cos" function is
fn(f64) -> f64).

Example usage from man dlopen:

#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
// Defines LIBM_SO (which will be a string such as "libm.so.6")
#include <gnu/lib-names.h>

// the type signature used by our cosine function
typedef double (*trig_func)(double);

int main() {
    char *error;

    // load the libm library into memory
    void *handle = dlopen(LIBM_SO, RTLD_LAZY);

    // handle loading failures
    if (!handle) {
        fprintf(stderr, "unable to load libm: %sn", dlerror());
        return EXIT_FAILURE;
    }

    // Clear any existing errors
    dlerror();

    // get a pointer to the "cos" symbol and cast it to the right type
    trig_func cosine = (trig_func) dlsym(handle, "cos");

    // were we able to find the symbol?
    error = dlerror();
    if (error != NULL) {
        fprintf(stderr, "cos not found: %sn", error);
        return EXIT_FAILURE;
    }

    // use our cosine function
    printf("cos(2.0) = %fn", (*cosine)(2.0));

    // cleanup and exit
    dlclose(handle);
    return EXIT_SUCCESS;
}

The story is almost identical for Windows, except LoadLibraryA(),
GetProcAddress(), and FreeLibrary() are used instead
of dlopen(), dlsym(), and dlclose(), respectively.

The libloading crate provides a high quality Rust interface to
the underlying platform’s dynamic loading mechanism.

Determining the Plugin Interface

Link to heading

The first step is to define a common interface that all plugins should satisfy.
This should be placed in some sort of “core” crate that both plugins and the
main application depend on.

This will usually take the form of a trait.

// core/src/lib.rs

pub trait Function {
    fn call(&self, args: &[f64]) -> Result<f64, InvocationError>;

    /// Help text that may be used to display information about this function.
    fn help(&self) -> Option<&str> {
        None
    }
}

pub enum InvocationError {
    InvalidArgumentCount { expected: usize, found: usize },
    Other { msg: String },
}

Now we’ve defined the application-level API, we also need a way to declare
plugins so they’re accessible when dynamically loading. This isn’t difficult,
but there are a couple gotchas to keep in mind to prevent undesired behaviour
(UB, crashes, etc.).

Some things to keep in mind:

  • Rust doesn’t have a stable ABI, meaning different compiler versions can
    generate incompatible code, and
  • Different versions of the core crate may have different definitions of the
    Function trait
  • Each plugin will need to have some sort of register() function so it can
    construct a Function instance and give the application a Box<dyn Function>
    (we need dynamic dispatch because plugin registration happens at runtime
    and static dispatch requires knowing types at compile time)
  • To avoid freeing memory allocated by a different allocator, each plugin
    will need to provide an explicit free_plugin() function, or the plugin and
    application both need to be using the same allocator

To prevent plugin authors from needing to deal with this themselves, we’ll
provide a export_plugin!() macro that populates some PluginDeclaration
struct with version numbers and a pointer to the register() function provided
by a user.

The PluginDeclaration struct itself is quite simple:

// core/src/lib.rs

pub struct PluginDeclaration {
    pub rustc_version: &'static str,
    pub core_version: &'static str,
    pub register: unsafe extern "C" fn(&mut dyn PluginRegistrar),
}

With the PluginRegistrar being a trait that has a single method.

// core/src/lib.rs

pub trait PluginRegistrar {
    fn register_function(&mut self, name: &str, function: Box<dyn Function>);
}

To get the version of rustc, we’ll add a build.rs script to the core crate
and pass the version number through as an environment variable.

// core/build.rs

fn main() {
    let version = rustc_version::version().unwrap();
    println!("cargo:rustc-env=RUSTC_VERSION={}", version);
}

We’re using the rustc_version crate to fetch rustc’s
version number. Don’t forget to add it to core/Cargo.toml as a build
dependency:

$ cd core
$ cargo add rustc_version
    Updating 'https://github.com/rust-lang/crates.io-index' index
      Adding rustc_version v0.2.3 to build-dependencies

Now all we need to do is embed the version numbers as static strings.

// core/src/lib.rs

pub static CORE_VERSION: &str = env!("CARGO_PKG_VERSION");
pub static RUSTC_VERSION: &str = env!("RUSTC_VERSION");

Our export_plugin!() macro now becomes almost trivial:

// core/src/lib.rs

#[macro_export]
macro_rules! export_plugin {
    ($register:expr) => {
        #[doc(hidden)]
        #[no_mangle]
        pub static plugin_declaration: $crate::PluginDeclaration = $crate::PluginDeclaration {
            rustc_version: $crate::RUSTC_VERSION,
            core_version: $crate::CORE_VERSION,
            register: $register,
        };
    };
}

Creating a Plugin

Link to heading

Now we have a public plugin interface and a mechanism for registering new
plugins, lets actually create one.

First we’ll need to create a plugins_random crate and add it to the workspace.

$ cargo new --lib random --name plugins_random
     Created library `plugins_random` package
$ cat Cargo.toml
[workspace]
members = ["core", "random"]

Next, make sure the plugins_random crate pulls in plugins_core.

$ cd random
$ cargo add ../core
    Updating 'https://github.com/rust-lang/crates.io-index' index
      Adding plugins_core (unknown version) to dependencies

This crate will need to be compiled as a dynamic library (*.so in *nix,
*.dll on Windows) so it can be loaded at runtime.

# random/Cargo.toml

[package]
name = "plugins_random"
version = "0.1.0"
authors = ["Michael Bryan <michaelfbryan@gmail.com>"]
edition = "2018"

[lib]
crate-type = ["cdylib"]

[dependencies]
plugins_core = { path = "../core" }

Recompiling should show a libplugins_random.so file in the target/ directory.

$ cargo build --all
   Compiling semver-parser v0.7.0
   Compiling semver v0.9.0
   Compiling rustc_version v0.2.3
   Compiling plugins_core v0.1.0 (/home/michael/Documents/plugins/core)
   Compiling plugins_random v0.1.0 (/home/michael/Documents/plugins/random)
    Finished dev [unoptimized + debuginfo] target(s) in 1.32s
$ ls ../target/debug
build deps examples incremental libplugins_core.d libplugins_core.rlib
libplugins_random.d libplugins_random.so

Now things are set up, we can start implementing our random() plugin.

Looking at the Random Integer Generator page, retrieving a set of
random integers is just a case of sending a GET request to
https://www.random.org/integers/.

For example, to get 10 numbers from 1 to 6 in base 10 and one number per line:

$ curl 'https://www.random.org/integers/?num=10&min=1&max=6&col=1&base=10&format=plain'
5
2
6
4
5
2
1
4
1
3

This turns out to be almost trivial to implement thanks to the
reqwest crate.

First we’ll create a helper struct for the arguments to pass to random.org.

// random/src/lib.rs

struct RequestInfo {
    min: i32,
    max: i32,
}

impl RequestInfo {
    pub fn format(&self) -> String {
        format!(
            "https://www.random.org/integers/?num=1&min={}&max={}&col=1&base=10&format=plain",
            self.min, self.max
        )
    }
}

Then write a function that calls reqwest::get() using the formatted URL and
parses the response body.

// random/src/lib.rs

fn fetch(request: RequestInfo) -> Result<f64, InvocationError> {
    let url = request.format();
    let response_body = reqwest::get(&url)?.text()?;
    response_body.trim().parse().map_err(Into::into)
}

To make ? work nicely, I’ve also added a From impl which lets us create an
InvocationError from anything that is ToString (which all
std::error::Error types implement).

// core/src/lib.rs

impl<S: ToString> From<S> for InvocationError {
    fn from(other: S) -> InvocationError {
        InvocationError::Other {
            msg: other.to_string(),
        }
    }
}

Finally, we just need to create a Random struct which will implement our
Function interface.

// random/src/lib.rs

pub struct Random;

impl Function for Random {
    fn call(&self, _args: &[f64]) -> Result<f64, InvocationError> {
        fetch(RequestInfo { min: 0, max: 100 })
    }
}

Ideally our random() function should have a couple overloads so users can
tweak the random number’s properties.

// get a random number between 0 and 100
fn random() -> f64;
// get a random number between 0 and max
fn random(max: f64) -> f64;
// get a random number between min and max
fn random(min: f64, max: f64) -> f64;

The logic for turning the &[f64] args into a RequestInfo can be neatly
extracted into its own function.

// random/src/lib.rs

fn parse_args(args: &[f64]) -> Result<RequestInfo, InvocationError> {
    match args.len() {
        0 => Ok(RequestInfo { min: 0, max: 100 }),
        1 => Ok(RequestInfo {
            min: 0,
            max: args[0].round() as i32,
        }),
        2 => Ok(RequestInfo {
            min: args[0].round() as i32,
            max: args[1].round() as i32,
        }),
        _ => Err("0, 1, or 2 arguments are required".into()),
    }
}

And then we just need to update the Function impl accordingly.

// random/src/lib.rs

impl Function for Random {
    fn call(&self, args: &[f64]) -> Result<f64, InvocationError> {
        parse_args(args).and_then(fetch)
    }
}

Now our random() function is fully implemented, we just need to make a
register() function and call plugins_core::export_plugin!().

// random/src/lib.rs

plugins_core::export_plugin!(register);

extern "C" fn register(registrar: &mut dyn PluginRegistrar) {
    registrar.register_function("random", Box::new(Random));
}

Loading Plugins

Link to heading

Now we’ve defined a plugin we need a way to load it into memory and use it as
part of our application.

The first step is to create a new crate and add some dependencies.

$ cargo new --lib --name plugins_app app
$ cat Cargo.toml
[workspace]
members = ["core", "random", "app"]
$ cd app
$ cargo add libloading ../core
    Updating 'https://github.com/rust-lang/crates.io-index' index
      Adding libloading v0.5.2 to dependencies
      Adding plugins_core (unknown version) to dependencies

When a library is loaded into memory, we need to make sure that it outlives
anything created from it. For example, a trait object’s vtable (and all the
functions it points to) is embedded in the library’s code. If we tried to invoke
a plugin object’s methods after its parent library was unloaded from memory,
we’d try to execute garbage and crash the entire application.

This means we need a way to make sure plugins can’t outlive the library they
were loaded from.

We’ll do this using the Proxy Pattern.

// app/src/main.rs

/// A proxy object which wraps a [`Function`] and makes sure it can't outlive
/// the library it came from.
pub struct FunctionProxy {
    function: Box<dyn Function>,
    _lib: Rc<Library>,
}

impl Function for FunctionProxy {
    fn call(&self, args: &[f64]) -> Result<f64, InvocationError> {
        self.function.call(args)
    }

    fn help(&self) -> Option<&str> {
        self.function.help()
    }
}

We also need something which can contain all loaded plugins.

// app/src/main.rs

pub struct ExternalFunctions {
    functions: HashMap<String, FunctionProxy>,
    libraries: Vec<Rc<Library>>,
}

impl ExternalFunctions {
    pub fn new() -> ExternalFunctions {
        ExternalFunctions::default()
    }

    pub fn load<P: AsRef<OsStr>>(&mut self, library_path: P) -> io::Result<()> {
        unimplemented!()
    }
}

The ExternalFunctions::load() method is the real meat and potatoes of our
plugin system. It’s where we:

  1. Load the library into memory
  2. Get a reference to the static PluginDeclaration
  3. Check the rustc and plugins_core versions match
  4. Create a PluginRegistrar which will create FunctionProxys associated with
    the library
  5. Pass the PluginRegistrar to the plugin’s register() function
  6. Add any loaded plugins to the internal functions map

The PluginRegistrar type itself is almost trivial:

// app/src/main.rs

struct PluginRegistrar {
    functions: HashMap<String, FunctionProxy>,
    lib: Rc<Library>,
}

impl PluginRegistrar {
    fn new(lib: Rc<Library>) -> PluginRegistrar {
        PluginRegistrar {
            lib,
            functions: HashMap::default(),
        }
    }
}

impl plugins_core::PluginRegistrar for PluginRegistrar {
    fn register_function(&mut self, name: &str, function: Box<dyn Function>) {
        let proxy = FunctionProxy {
            function,
            _lib: Rc::clone(&self.lib),
        };
        self.functions.insert(name.to_string(), proxy);
    }
}

And now our PluginRegistrar helper is implemented, we have everything required
to complete ExternalFunctions::load().

// app/src/main.rs

impl ExternalFunctions {
    ...

    /// Load a plugin library and add all contained functions to the internal
    /// function table.
    ///
    /// # Safety
    ///
    /// A plugin library **must** be implemented using the
    /// [`plugins_core::plugin_declaration!()`] macro. Trying manually implement
    /// a plugin without going through that macro will result in undefined
    /// behaviour.
    pub unsafe fn load<P: AsRef<OsStr>>(
        &mut self,
        library_path: P,
    ) -> io::Result<()> {
        // load the library into memory
        let library = Rc::new(Library::new(library_path)?);

        // get a pointer to the plugin_declaration symbol.
        let decl = library
            .get::<*mut PluginDeclaration>(b"plugin_declaration")?
            .read();

        // version checks to prevent accidental ABI incompatibilities
        if decl.rustc_version != plugins_core::RUSTC_VERSION
            || decl.core_version != plugins_core::CORE_VERSION
        {
            return Err(io::Error::new(
                io::ErrorKind::Other,
                "Version mismatch",
            ));
        }

        let mut registrar = PluginRegistrar::new(Rc::clone(&library));

        (decl.register)(&mut registrar);

        // add all loaded plugins to the functions map
        self.functions.extend(registrar.functions);
        // and make sure ExternalFunctions keeps a reference to the library
        self.libraries.push(library);

        Ok(())
    }
}

Note the Safety section in the function’s doc-comments. The process of
loading a plugin is inherently unsafe (the compiler can’t guarantee
whatever is behind the plugin_declaration symbol is a PluginDeclaration)
and this section documents the contract that must be upheld.

Using the Plugin

Link to heading

At this point we’ve actually completed the plugin system. The only thing left is
to demonstrate it works and start using the thing.

For our purposes, it should be good enough to create a command-line app that
loads a library then invokes a function by name, passing in any specified
arguments.

Usage: app <plugin-path> <function> <args>...

First we’ll create a quick Args struct to parse our command-line arguments
into.

// app/src/main.rs

struct Args {
    plugin_library: PathBuf,
    function: String,
    arguments: Vec<f64>,
}

Then hack together a quick’n’dirty command-line parser. Real applications should
prefer to use something like clap or structopt instead.

// app/src/main.rs

impl Args {
    fn parse(mut args: impl Iterator<Item = String>) -> Option<Args> {
        let plugin_library = PathBuf::from(args.next()?);
        let function = args.next()?;
        let mut arguments = Vec::new();

        for arg in args {
            arguments.push(arg.parse().ok()?);
        }

        Some(Args {
            plugin_library,
            function,
            arguments,
        })
    }
}

We’ll also need a way to call() a function by name.

// app/src/main.rs

impl ExternalFunctions {
    ...

    pub fn call(&self, function: &str, arguments: &[f64]) -> Result<f64, InvocationError> {
        self.functions
            .get(function)
            .ok_or_else(|| format!(""{}" not found", function))?
            .call(arguments)
    }
}

By default a cdylib will use the system allocator, but executables aren’t guaranteed
to use

According to the docs from std::alloc,

Currently the default global allocator is unspecified. Libraries, however,
like cdylibs and staticlibs are guaranteed to use the System by default.

To make sure there’s no chance of allocator mismatch (i.e. a plugin allocates
a String using the System allocator and we try to free it using Jemalloc) we
need to explicitly declare that the app uses the System allocator.

// app/src/main.rs

use std::alloc::System;

#[global_allocator]
static ALLOCATOR: System = System;

And finally, we can write main()’s body.

// app/src/main.rs

fn main() {
    // parse arguments
    let args = env::args().skip(1);
    let args = Args::parse(args)
        .expect("Usage: app <plugin-path> <function> <args>...");

    // create our functions table and load the plugin
    let mut functions = ExternalFunctions::new();

    unsafe {
        functions
            .load(&args.plugin_library)
            .expect("Function loading failed");
    }

    // then call the function
    let result = functions
        .call(&args.function, &args.arguments)
        .expect("Invocation failed");

    // print out the result
    println!(
        "{}({}) = {}",
        args.function,
        args.arguments
            .iter()
            .map(ToString::to_string)
            .collect::<Vec<_>>()
            .join(", "),
        result
    );
}

If everything goes to plan, the app tool should Just Work.

$ cargo run -- ../target/release/libplugins_random.so random
random() = 40
$ cargo run -- ../target/release/libplugins_random.so random 42
random(42) = 15
$ cargo run -- ../target/release/libplugins_random.so random 42 64
random(42, 64) = 54

# Note: the function doesn't support 3 arguments
$ cargo run -- ../target/release/libplugins_random.so random 1 2 3
thread 'main' panicked at 'Invocation failed: Other { msg: "0, 1, or 2 arguments are required" }', src/libcore/result.rs:1165:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.

If a plugin author forgot to invoke the export_plugin!() macro, they may see
an error like this:

$ cargo run -- ../target/debug/libplugins_random.so random
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s
     Running `/home/michael/Documents/plugins/target/debug/plugins_app ../target/debug/libplugins_random.so random`
thread 'main' panicked at 'Function loading failed: Custom { kind: Other, error: "../target/debug/libplugins_random.so: undefined symbol: plugin_declaration" }', src/libcore/result.rs:1165:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.

This is saying we couldn’t find the plugin_declaration symbol. You can
use the nm tool to help with troubleshooting, it shows all symbols exported by
a library.

nm ../target/release/libplugins_random.so  | grep plugin
00000000004967b0 D plugin_declaration
0000000000056c60 t _ZN12plugins_core8Function4help17hc92b9e8d4917f964E
0000000000057f60 t _ZN14plugins_random8register17hd43ebfdd726021a4E
0000000000057f80 t _ZN65_$LT$plugins_random..Random$u20$as$u20$plugins_core..Function$GT$4call17h7434ef9b1f1ca59eE
00000000000590f0 t _ZN78_$LT$plugins_core..InvocationError$u20$as$u20$core..convert..From$LT$S$GT$$GT$4from17h3a759bcd267b48a1E

And there you have it, a relatively simple, yet safe and robust, plugin
system which you can use in your own projects.

See Also

Link to heading

  • Building a Simple C++ Cross-platform Plugin System
  • The (unofficial) Rust FFI Guide: Dynamic Loading & Plugins
  • Plugins in C
  • The code written alongside this article on GitHub

3.1. ABI unstability, it’s much worse than it seems

I recently had this very late realization about ABI stability in Rust. Up until
this point I thought that even though Rust’s ABI is unstable, one could
dynamically load a library safely as long as both the library and the main
binary were compiled with the same exact compiler/std/etc version. I had read
this many times in forums like
this
one on Reddit and in blogposts such as
«Plugins in Rust»,
so I assumed it was true.

But turns out that the ABI may not only break between compiler versions, but
also compiler runs. rustc doesn’t guarantee that a layout is going to be the
same in different executions. This is proved by
rust-lang/compiler-team#457,
the new unstable -Z randomize-layout flag. It’s pretty much self-explanatory:
it randomly reorders repr(rust) layouts for testing purposes. The same thing
could happen in the future without an explicit flag; an optimization may cause
the repr(rust) layouts to change between compilation runs. It’s briefly
mentioned in
the Rust
reference as well:

Type layout can be changed with each compilation. Instead of trying to document
exactly what is done, we only document what is guaranteed today.

Props to the devs at the #black-magic channel in
Rust’s Discord server, who helped me understand
this. Specially Yandros and Kixiron, both of them very respectable contributors
to the Rust compiler/community.

This basically means that we are forced to stick to the C ABI with #[repr(C)],
and that we should use abi_stable
in order to have access to a
stable std library as well, instead of re-implementing everything ourselves
from scratch. On the positive side, it means that plugins could be implemented
in any language, but that wasn’t important for Tremor’s case since the beginning
anyway.

3.2. Getting a simple example running

I’ve created the
pdk-experiments repository,
where I’ll write various examples of how the plugin system might work. The first
experiment is in the
dynamic-simple
directory, with a small example of how to dynamically load plugins with Rust.

We first need a crate called common, which defines the interface shared by the
plugin and the runtime in the main binary. In this case it’s just a pointer to a
function with the C ABI. We can specify the ABI with extern "C", or simply
extern, as "C" is already its default value [1]. To keep it simple
it’ll just compute the minimum between two integers:

common/src/lib.rs

pub type MinFunction = unsafe extern fn(i32, i32) -> i32;

With it, the plugin crate may export its own implementation. In this case I’ll
declare a static variable, but the example showcases how extern may work as
well. Since we want to use the C ABI, we’ll have to specify crate-type as
cdylib in our Cargo.toml. Note that #[no_mangle] is necessary so that the
variable’s name isn’t mangled and
we can access it when dynamically loading the library.

plugin-sample/src/lib.rs

#[no_mangle]
pub static with_static: MinFunction = min;

pub extern fn min(a: i32, b: i32) -> i32 {
    a.min(b)
}

Finally, the main binary can load the library with libloading
,
which requires a bit of unsafe. I was looking forward to using a different
library because of how easy it seems to end up with undefined behaviour in that
case. I found out sharedlib
was abandoned, as no commits had been
made since 2017, leaving dlopen
as the only alternative. Which was
updated two years ago as well, but their GitHub repo seemed somewhat active in
comparison.

For now, I’ll just use libloading for being the most popular crate, and
perhaps I’ll consider using dlopen in the future. In terms of relevant
features and performance they’re pretty close anyway [2]. Here’s
what the code looks like:

src/main.rs

fn run_plugin(path: &str) -> Result<(), libloading::Error> {
    unsafe {
        let library = Library::new(path)?;
        let min = library.get::<*mut MinFunction>(b"plugin_function")?.read();
        println!("Running plugin:");
        println!("  min(1, 2): {}", min(1, 2));
        println!("  min(-10, 10): {}", min(-10, 10));
        println!("  min(2000, 2000): {}", min(2000, 2000));
    }

    Ok(())
}

We can run it with the following commands (though the Makefile in the repo
will do everything for you):

$ cd plugin-sample
$ cargo build --release
$ cd ..
$ cargo run --release -- plugin-sample/target/release/libplugin_sample.so
Running plugin:
  min(1, 2): 1
  min(-10, 10): -10
  min(2000, 2000): 2000

Cool! This raises a few questions that I should learn more about:

  1. Since we’re using the C ABI, is it perhaps best to declare the bindings in C?
    The common crate I introduced earlier could just be a header.

  2. There are many options to configure crate-type as a
    dynamic library. What are
    they and which one should I choose?

  3. I faintly remember that rlib files are Rust-only objects with additional
    metadata for things like generics. Could that possibly work at runtime? As in,
    is there an equivalent to
    COM in Rust, or maybe
    like JAR files in Java?

3.3. Generating bindings

The public interface for the plugins can be written either in Rust (thanks to
extern "C") or directly in C. There are two commonly used projects when
writing bindings:

  • rust-bindgen generates Rust
    bindings from C code

  • cbindgen is the opposite; it generates C
    bindings from Rust code.

Some examples of its usage:

  • hyper
    is a crate completely written in Rust that exposes C
    headers for compatibility, so it uses cbindgen to generate them
    automatically.

  • pipewire_rs
    exposes the interface of
    PipeWire, written in C, so that it’s also available
    from Rust, thanks to rust-bindgen.

Since we’re going to write the plugin system in Rust, the most appropiate choice
would be to use Rust for the interface as well. And if we wanted to make the
plugin interface available to other languages — which is not a concern right
now — it’d be as “easy” as setting up cbindgen.

3.4. crate-type values

  • dylib

  • cdylib

Once again, this difference has to do with the ABIs in the dynamic library
[3]. cdylib is meant for linking into C/C++ programs (so it strips away
all functions that aren’t publicly exported), and dylib is meant for Rust
libraries.

When compiling the previous example with dylib, the resulting shared object
for the plugin has a size of 4.8Mb, whereas with cdylib it’s just 2.9Mb. So
while both of these will work for our C ABI, cdylib is clearly the more
appropiate choice.

3.5. rlib files

rlib is another value for crate-type to generate Rust static libraries,
which can then be imported with extern crate crate_name [3]. But since
rlib files are static libraries, they can’t be loaded at runtime, so they’re
of no use in a plugin system.

Here’s a crazy idea though: What if the rlib files were dynamically loaded as
plugins with the help of MIRI? I recently
learned about it, and quoting its official documentation:

[MIRI is] an experimental interpreter for Rust’s mid-level intermediate
representation (MIR). It can run binaries and test suites of cargo projects and
detect certain classes of undefined behavior.

You can use Miri to emulate programs on other targets, e.g., to ensure that
byte-level data manipulation works correctly both on little-endian and
big-endian systems.

Hmm. Could it possibly be used to interpret Rust code? In some way this would be
very similar to using WebAssembly, but theoretically with less friction, as MIR
is specific to Rust and plugin development would be as easy as in the case of
dynamic loading with Rust-to-Rust FFI. A few things to consider:

  1. Is this even possible?

    The Rust compiler itself uses MIRI to evaluate constant expressions
    [4] via the
    rustc_mir
    crate. But taking a quick look it seems to be created specifically for the
    compiler, at a very low level, and without that much documentation. Plus, it’s
    nightly-only. It does seem possible, but I wasn’t able to get a simple example
    working.

  2. Is MIR stable?

    MIR is unfortunately unstable [5], so we’d have the same
    incompatibility problems between plugins and the main binary.

  3. Is the overhead of MIRI worth it?

    Considering the previous answers, no, but it was cool to consider and learn
    about :)

bazuka5801


  • #1

Всем доброго :)
Представляю вашему вниманию первый курс по написанию плагинов для игры RUST.
Пройдем путь с 0 до разработчика плагинов.

Уроки буду публиковать под постом, пишите свои комментарии, пожелания :)

Ссылка на плейлист

bazuka5801


  • #2

Давно были планы записать видеоуроки по созданию плагинов для нашей с вами игрушки — Rust.
Вдохновение стукнуло, можем начинать, плейлист активно пополняется новыми видео, пишите в комментариях:

:one:

Ваши идеи для плагинов?

:wink:

:two:

Что нам может быть добавить/поменять в формате

:heavy_check_mark:

:three:

Ваши впечатления

:heart_eyes:

:slight_smile:

P.S. В следующем видео исправим качество на 1080p :)

bazuka5801


  • #3

Продолжаем серию уроков по написанию плагинов.
#2 Урок — Установка сервера с помощью PowerShell.

Материалы: https://pastebin.com/jxMjM2Hy

Ставим лайки, подписываемся и ставим себе сервер, завтра уже будем работать с VS code и напишем первый плагин.

bazuka5801


  • #4

?

?

3 УРОК

?

?

Сегодня мы напишем первый плагин :)
Я записываю свои уроки на импровизации, и этот хорошо показывает такой формат, его ошибки, как вам в целом?
Пишите свои идеи плагинов в комментарии, которые добавить в курс.

?

Автору лучшей идеи я отправлю 100руб!

Материалы: https://pastebin.com/igWtUYXw

bazuka5801


  • #5

?

?

Урок номер 4 готов!

?

Один подписчик попросил поработать с GUI — пишем плагин :)

?

Cуть плагина:
При попадании в объект, у которого есть хп, мы отображаем игроку сколько хп осталось.
Приятного просмотра

?

P.S. Напоминаю о денежном конкурсе, см. предыдущий поста и пиши свою идею для плагина :)

Материалы: https://pastebin.com/yfPn1R3w

Последнее редактирование: 16 Июн 2020

bazuka5801


  • #6

?

5 Урок уже на канале :)

?

Пройдёмся по хукам, таймерам, словарям и классам, затронули немного ООП.

?

Рекомендую смотреть на скорости x2 для экономии вашего времени.

bazuka5801


  • #7

?

?

6 Урок на канале )))
Сегодня мы разберемся как работать с конфигурацией и пермишенами, ну и есессно сперва применим это на практике!)

bazuka5801


  • #8

?

?

Урок №7 уже доступен — бегом писать плагины и становиться мощными разработчиками плагинов

?

??

В этом урооке мы разберемся как работать с persistent data и встроенной локализацией в oxide

Материалы: https://pastebin.com/dCyfG6pV

?

Это последнее видео курса, но дальше я буду делать новый, так скажем, для уровня продолжающие, мы будем углубляться в разные темы и делать что-то посложнее)

  • #9

Сел я тако

Давно были планы записать видеоуроки по созданию плагинов для нашей с вами игрушки — Rust.
Вдохновение стукнуло, можем начинать, плейлист активно пополняется новыми видео, пишите в комментариях:

:one:

Ваши идеи для плагинов?

:wink:

:two:

Что нам может быть добавить/поменять в формате

:heavy_check_mark:

:three:

Ваши впечатления

:heart_eyes:

:slight_smile:

P.S. В следующем видео исправим качество на 1080p :)

Сел я такой сегодня, плагины учится писать. Человек знающий, сказал подскажет. И понял я, что ничего не понял. Но в любом случае, буду пытаться

Mercury


  • #10

Сел я тако

Сел я такой сегодня, плагины учится писать. Человек знающий, сказал подскажет. И понял я, что ничего не понял. Но в любом случае, буду пытаться

Не сдавайся только, ошибок будет много и это хороший знак, это не значит, что ты не можешь или не дано, это значит — что ты делаешь все правильно и стараешься!

Питай больше информации в интернете, гугли, читай, смотри, слушай и все получится! :)

myst1c


  • #11

Можете подсказать как установить сервер через повершел как на 2 видео я скачиваю эту папку но там нет config-defaultRust_default_defaut.ps1

myst1c


  • #12

Можете подсказать как установить сервер через повершел как на 2 видео я скачиваю эту папку но там нет config-defaultRust_default_defaut.ps1

Всё разобрался там всё настраивается через PowerShell.

aleks78888


  • #1

  • Для любого плагина игрок должен создать текстовый файл. Назвать его можно как угодно. Но этого мало, нужно изменить расширение на load.
  • Мы создали файл, он должен пустым, и там ничего не должно быть написано. Кстати, советую, для написания плагина, да и для пользования любыми тестовыми файлами, использовать программку Нот айпад.
  • Итак, мы создали пустой «плагин». Что же делать дальше? Необходимо зайти на http://wiki.rustoxide.com/. Здесь вы можете найти всю информацию о плагинах, об их написании и т.п. Идем дальше – категория – туториалс-байзи плагин. Далее нам нужно скопировать первые четыре строки плагина, который мы сейчас будем создавать. Данные строки должны быть в каждом плагине. В принципе, эти строки редко используются, но все-таки советуется их написать.

    Потом мы копируем скелет вашего каждого плагина. Далее в видео вы узнаете, как этим всем пользоваться, для чего эти коды нужны, как правильно создавать плагин Rust, какие ошибки не следует допускать и из-за чего ваша работа может оказаться пустой тратой времени.

    Создать плагин Раст достаточно просто. Главное, вес делать по инструкции, не отходя от цели. Так как можно запутаться и намудрить. В общем, видео полезное. Смотрите и учитесь делать что-то полезное для игры и для себя в том числе.

Понравилась статья? Поделить с друзьями:
  • Как написать плагин для notepad
  • Как написать плагин для modx revo
  • Как написать плагин для minecraft на python
  • Как написать плагин для minecraft на java
  • Как написать плагин для kodi