1 of 87

Плагины на Rust

для распределённой СУБД

— технологический вызов

Мошкин Георгий

2 of 87

Георгий “Егор” Мошкин

  • ВМК МГУ
  • github.com/gmoshkin
  • picodata.io

2

3 of 87

О чём доклад?

  • Что такое плагины
  • Как это работает на низком уровне
  • Как с этим работать на rust
  • Примеры

3

4 of 87

Что такое плагины?

Пользовательский код внутри вашей программы

  • Расширение функционала
  • Недостающие фичи
  • Ускорение разработки
  • Кастомизация

4

5 of 87

Кому нужны плагины?

  • Media (photoshop, blender, …)
  • Text editors & IDE (neovim, vs code, …)
  • Gaming (modding)
  • Web (java, flash, javascript)
  • DBMS (postgres extensions, picodata plugins, …)
  • Compilers (gcc, clang, rust)
  • OS (drivers, eBPF)

5

6 of 87

Какие бывают плагины?

  • Интерпретируемые
    • lua, python, lisp, …
  • Байт-код виртуальные машины
    • wasm, jvm, .NET
  • Нативные (компилируемые)
    • С/C++, rust, …
  • Networking/IPC
    • LSP

6

7 of 87

Как сделать выбор?

  • Кто будет писать плагины?
  • Какой язык программирования?
    • язык хост программы
    • язык плагинов
  • Какие требования к производительности?

7

8 of 87

Нативные плагины

  • dynamically loaded library (dll)
    • aka shared object (so)
  • mmap(…, PROT_EXEC, …)

8

9 of 87

Что такое линкер?

9

gcc

main.c

main.o

ld

my_program

10 of 87

Статическая линковка

10

gcc

main.c

main.o

gcc

foo.c

libfoo.a

main.o

ld

my_program

libfoo.a

11 of 87

Динамическая линковка

11

gcc

main.c

main.o

gcc

foo.c

libfoo.so

main.o

ld

my_program

ld

my_program

libfoo.so

12 of 87

Статическая линковка

12

fn bar() -> i32 {

42

}

fn foo() -> i32 {

27 + bar()

}

006b20 <bar>:

mov $42,%eax

ret

006b30 <foo>:

push %rax

call 6b20 <bar>

add $27,%eax

ret

rustc

foo/src/main.rs

13 of 87

Статическая линковка

13

006b20 <bar>:

mov $42,%eax

ret

006b30 <foo>:

push %rax

call *0xdffd1(%rip)

add $27,%eax

ret

fn foo() -> i32 {

27 + bar::bar()

}

foo/src/main.rs

pub fn bar() -> i32 {

42

}

bar/src/lib.rs

$ nm target/debug/foo | grep bar

0000000000006b20 T _ZN3bar3bar17h2a…

rustc

14 of 87

Динамическая линковка

14

fn foo() -> i32 {

27 +

unsafe { bar() }

}

extern “C” {

fn bar() -> i32;

}

rustc

foo/src/main.rs

#[no_mangle]

pub extern “C”

fn bar() -> i32 {

42

}

bar/src/lib.rs

$ nm target/debug/foo | grep bar

U bar

006b30 <foo>:

push %rax

call *0xdffd1(%rip)

add $27,%eax

ret

006b20 <bar>:

mov $42,%eax

ret

15 of 87

ELF

  • Executable and Linkable Format
  • nm, readelf, objdump
  • Плоское пространство имён

15

16 of 87

Name mangling

16

$ gcc test.c -o test-c

$ nm test-c | grep my_func

0000000000001149 T my_func

#include <stdio.h>

int my_func(int n) {

return 27 + n;

}

int main() {

int i = my_func(42);

printf(“%d\n”, i);

return 0;

}

test.c

17 of 87

Name mangling

17

test.cpp

#include <stdio.h>

int my_func(int n) {

return 27 + n;

}

int main() {

int i = my_func(42);

printf(“%d\n”, i);

return 0;

}

$ g++ test.cpp -o test-cpp

$ nm test-cpp | grep my_func

0000000000001149 T _Z7my_funci

18 of 87

Name mangling

18

test.cpp

int my_func(int n) {

return 27 + n;

}

int my_func(char *c) {

return strlen(c);

}

$ g++ test.cpp -o test-cpp

$ nm test-cpp | grep my_func

0000000000001169 T _Z7my_funci

000000000000117c T _Z7my_funcPc

19 of 87

Name mangling

19

test.rs

fn my_func(n: i32) -> i32 {

27 + n

}

fn main() {

let i = my_func(42);

println!(“{i}”);

}

$ rustc test.rs -o test-rust

$ nm test-rust | grep my_func

… _ZN4test7my_func17h39758bdd57c88daeE

20 of 87

Name mangling

20

$ rustc test.rs

=> _ZN4test7my_func17h39758bdd57c88daeE

$ rustc test.rs -C metadata=XXX

=> _ZN4test7my_func17he28f0d7cc46e3475E

21 of 87

Name mangling

  • version sensitive
  • unstable! v2
  • dynamic linking?

21

22 of 87

Name mangling

22

#[no_mangle]

pub extern “C” fn bar() -> i32 {

42

}

bar/src/lib.rs

23 of 87

Name mangling

23

#[export_name = “bar”]

pub extern “C” fn bar() -> i32 {

42

}

bar/src/lib.rs

24 of 87

ABI

24

#[no_mangle]

pub extern “C” fn bar() -> i32 {

42

}

bar/src/lib.rs

25 of 87

ABI

Application Binary Interface

https://doc.rust-lang.org/reference/items/external-blocks.html#abi:

  • extern "Rust" - default (unstable)
  • extern "C" - same as C
  • extern "win64" - Windows
  • extern "aapcs" - ARM
  • extern "efiapi" - UEFI

25

26 of 87

ABI

26

$ rustc test.rs

$ objdump -d test

013370 <add>:

add %esi,%edi

mov %edi,%eax

ret

069420 <main>:

mov $0xd,%edi

mov $0x25,%esi

call 013370 <add>

sub $0x2a,%eax

...

test.rs

fn add(a:i32,b:i32)->i32 {

a + b

}

fn main() {

let i = add(13,37)-42;

println!(“{i}”);

}

27 of 87

ABI

27

test.rs

fn add(a:i32,b:i32)->i32 {

a + b

}

fn main() {

let i = add(13,37)-42;

println!(“{i}”);

}

> rustc.exe test.rs

> dumpbin.exe /disasm test.exe

add:

add ecx,edx

mov eax,ecx

ret

main:

mov ecx,0xd

mov edx,0x25

call add

sub eax,0x2a

...

28 of 87

FFI

  • Foreign function interface
  • FFI = C-ABI = lingua franca
  • extern “C”
  • #[no_mangle]

28

29 of 87

Динамическая линковка

29

fn foo() -> i32 {

27 + unsafe { bar() }

}

extern “C” {

fn bar() -> i32;

}

foo/src/main.rs

#[no_mangle]

pub extern “C” fn bar() -> i32 {

42

}

bar/src/lib.rs

30 of 87

Пример: Динамическая линковка

30

$ tree dynamic-library-example

dynamic-library-example

├── application

│ ├── Cargo.toml

│ └── src

│ └── main.rs

├── Cargo.toml

└── library

├── Cargo.toml

└── src

└── lib.rs

31 of 87

Пример: Динамическая линковка

31

$ cat application/Cargo.toml

[package]

name = "application"

version = "0.1.0"

edition = "2021"

$ cat Cargo.toml

[workspace]

members = ["application","library"]

$ cat library/Cargo.toml

[package]

name = "library"

version = "0.1.0"

edition = "2021"

[lib]

crate-type = ["cdylib"]

32 of 87

Пример: Динамическая линковка

32

$ cat application/src/main.rs

fn main() {

let result = unsafe { add(1, 1) };

println!("1 + 1 = {result}");

}

extern "C" {

fn add(a: u64, b: u64) -> u64;

}

$ cat library/src/lib.rs

#[no_mangle]

pub extern "C" fn add(a: u64, b: u64) -> u64 {

a + b

}

33 of 87

Пример: Динамическая линковка

33

$ cargo build

...

error: linking with `cc` failed: exit status: 1

|

= note: /usr/bin/ld: target/d…a.rcgu.o: in function `application::main':

…/application/src/main.rs:2: undefined reference to `add'

collect2: error: ld returned 1 exit status

= note: use the `cargo:rustc-link-lib` directive to specify the native libraries to link with Cargo (see https://doc.rust-lang.org/cargo/reference/build-scripts.html#rustc-link-lib)

34 of 87

rustc-link-lib

34

$ cat application/build.rs

fn main() {

println!("cargo:rustc-link-lib=dylib=library");

}

$ tree dynamic-library-example

dynamic-library-example

├── application

│ ├── Cargo.toml

│ ├── build.rs

│ └── src

│ └── main.rs

└── ...

35 of 87

rustc-link-lib

35

$ cargo build

...

error: linking with `cc` failed: exit status: 1

|

= note: /usr/bin/ld:

cannot find -llibrary: No such file or directory

collect2: error: ld returned 1 exit status

36 of 87

rustc-link-search

36

$ cat application/build.rs

fn main() {

let out_dir = std::env::var("OUT_DIR").unwrap();

let lib_dir = format!(“{out_dir}/../../..”);

println!("cargo:rustc-link-search=native={lib_dir}");

println!("cargo:rustc-link-lib=dylib=library");

}

$ cargo build --all

Compiling application v0.1.0 (.../application)

Compiling library v0.1.0 (.../library)

Finished `dev` profile [debug] target(s) in 0.47s

37 of 87

37

$ ./target/debug/application

./target/debug/application:

error while loading shared libraries: liblibrary.so:

cannot open shared object file: No such file or directory

$ ldd ./target/debug/application

linux-vdso.so.1 (...)

liblibrary.so => not found

libgcc_s.so.1 => /lib/…/libgcc_s.so.1 (...)

libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (...)

/lib64/ld-linux-x86-64.so.2 (...)

38 of 87

LD_LIBRARY_PATH

38

$ LD_LIBRARY_PATH=./target/debug ./target/debug/application

1 + 1 = 2

$ LD_LIBRARY_PATH=./target/debug ldd ./target/debug/application

...

liblibrary.so => ./target/debug/liblibrary.so (...)

...

39 of 87

-rpath

39

$ cat application/build.rs

fn main() {

println!("cargo:rustc-link-arg=-Wl,-rpath=$ORIGIN");

println!("cargo:rustc-link-lib=dylib=library");

}

$ cargo build

$ objdump -x ./target/debug/application | grep RUNPATH

RUNPATH $ORIGIN

$ ./target/debug/application

1 + 1 = 2

$ ldd ./target/debug/application | grep liblibrary

liblibrary.so => /home/…/./target/debug/liblibrary.so

40 of 87

Динамическая линковка

  • rustc-link-lib (-llibrary)
  • rustc-link-search (-Lpath/to/library)
  • LD_LIBRARY_PATH/-rpath
  • неявные зависимости!

40

$ ./application

ld

liblibrary.so

ld

main()

41 of 87

dlopen

  • явный вызов
  • выбор путей
  • выбор символов

41

dlopen

$ ./application

main()

liblibrary.so

ld

42 of 87

Пример: dlopen

42

$ tree plugin-example

plugin-example

├── application

│ ├── Cargo.toml

│ └── src

│ └── main.rs

├── Cargo.toml

└── plugin

├── Cargo.toml

└── src

└── lib.rs

43 of 87

Пример: dlopen

43

$ cat application/Cargo.toml

[package]

name = "application"

version = "0.1.0"

edition = "2021"

[dependencies]

libc = "*"

$ cat Cargo.toml

[workspace]

members = ["application","plugin"]

$ cat plugin/Cargo.toml

[package]

name = "plugin"

version = "0.1.0"

edition = "2021"

[lib]

crate-type = ["cdylib"]

44 of 87

Пример: dlopen

44

$ cat plugin/src/lib.rs

#[no_mangle]

pub extern "C" fn add(a: u64, b: u64) -> u64 {

a + b

}

45 of 87

Пример: dlopen

45

fn main() {

let module = unsafe { load_library("libplugin.so") };

type fn_add_signature = extern "C" fn(u64, u64) -> u64;

let add: fn_add_signature = unsafe {

load_symbol(module, "add")

};

let result = (add)(1, 1);

println!("1 + 1 = {result}");

}

application/src/main.rs

46 of 87

Пример: dlopen

46

unsafe fn load_library(name: &str) -> *mut libc::c_void {

let exe_path = std::env::current_exe().unwrap();

let exe_dir = exe_path.parent().unwrap();

let plugin_path = exe_dir.join(name);

let plugin_path = plugin_path.to_str().unwrap();

let plugin_path = CString::new(plugin_path).unwrap();

let module = unsafe {

libc::dlopen(plugin_path.as_ptr(),

libc::RTLD_LOCAL | libc::RTLD_NOW)

};

assert!(!module.is_null());

module

}

47 of 87

Пример: dlopen

47

unsafe fn load_symbol<T>(

module: *mut libc::c_void, name: &str

) -> T {

let name = std::ffi::CString::new(name).unwrap();

let symbol: *mut libc::c_void

= libc::dlsym(module, name.as_ptr());

assert!(!symbol.is_null());

std::mem::transmute_copy(&symbol)

}

48 of 87

Пример: dlopen

48

$ cargo build

Finished `dev` profile [debug] target(s) in 0.07s

$ ldd ./target/debug/application | grep plugin

$ ./target/debug/application

1 + 1 = 2

49 of 87

Пример: dlopen

49

$ rm ./target/debug/libplugin.so

$ ./target/debug/application

thread 'main' panicked at application/src/main.rs:20:5:

assertion failed: !module.is_null()

stack backtrace:

...

3: application::load_library

at ./application/src/main.rs:20:5

4: application::main

at ./application/src/main.rs:2:18

...

50 of 87

Пример: dlopen

50

fn main() {

let module = unsafe { load_library("libplugin.so") };

type fn_add_signature = extern "C" fn(u64, u64) -> u64;

let add: fn_add_signature = unsafe {

load_symbol(module, "add")

};

let result = (add)(1, 1);

println!("1 + 1 = {result}");

}

application/src/main.rs

51 of 87

plugin-sdk

51

$ tree plugin-example

plugin-example

├── plugin-sdk

│ ├── Cargo.toml

│ └── src

│ └── lib.rs

└── ...

52 of 87

plugin-sdk

52

$ cat application/Cargo.toml

...

[dependencies]

plugin-sdk.path = “../plugin-sdk”

$ cat Cargo.toml

[workspace]

members = ["application","plugin",”plugin-sdk”]

$ cat plugin/Cargo.toml

...

[dependencies]

plugin-sdk.path = “../plugin-sdk”

53 of 87

plugin-sdk

53

pub type type_fn_add = extern "C" fn(a: u64, b: u64) -> u64;

#[no_mangle]

pub extern "C" fn add(a: u64, b: u64) -> u64 {…}

const TYPE_CHECK: plugin_sdk::type_fn_add = add;

plugin-sdk/src/lib.rs

plugin/src/lib.rs

fn main() {

let f: plugin_sdk::type_fn_add = unsafe {

load_symbol(m,"add")

};

}

application/src/main.rs

54 of 87

Safety

54

55 of 87

plugin-sdk

55

add()

app

plugin

56 of 87

plugin-sdk

56

app

plugin

57 of 87

plugin-sdk

57

/// Plugin exports symbols with this signature

pub type plugin_fn = extern "C" fn(...) -> ...;

/// Application exports symbols with this signature

extern “C” {

fn host_fn(...) -> ...;

}

plugin-sdk/src/lib.rs

58 of 87

plugin-sdk

58

fn main() {

...

println!("cargo:rustc-link-arg=-rdynamic");

...

}

application/build.rs

59 of 87

plugin-sdk

59

dlopen

app

plugin

dlopen(..., ... | RTLD_NOW)

60 of 87

plugin-sdk

60

app

plugin

ld

host_fn

extern “C” {

fn host_fn(...) -> ...;

}

61 of 87

plugin-sdk

61

app

plugin

plugin_fn

dlsym

dlsym(m, “plugin_fn”)

62 of 87

FFI

  • extern “C”
  • #[no_mangle]
  • crate-type = [“cdylib”]
  • -rdynamic
  • #[repr(C)]

62

63 of 87

repr(C)

63

test.c

<foo>:

movsbl (%rdi),%eax

movsbl 0x10(%rdi),%edx

add %edx,%eax

ret

struct my_struct {

char a;

long long b;

char c;

};

int foo(struct my_struct *s) {

return s->a + s->c;

}

64 of 87

repr(C)

64

test.rs

struct MyStruct {

a: u8,

b: u64,

c: u8,

};

fn foo(s: &MyStruct) -> i32 {

(s.a + s.c) as _

}

<foo>:

movzbl 0x8(%rdi),%eax

movzbl 0x9(%rdi),%edx

add %edx,%eax

ret

65 of 87

repr(C)

65

#[repr(Rust)]

struct Rust {

a: u8, b: i64, c: u8

}

a

-

-

-

-

-

-

-

b

b

b

b

b

b

b

b

c

-

-

-

-

-

-

-

b

b

b

b

b

b

b

b

a

c

-

-

-

-

-

-

#[repr(C)]

struct C {

a: u8, b: i64, c: u8

}

size_of::<Rust>() == 16

size_of::<C>() == 24

66 of 87

repr(C)

66

#[repr(Rust)]

enum Rust {

A, B, C

}

#[repr(C)]

enum C {

A, B, C

}

size_of::<Rust>() == 1

size_of::<C>() == 4

v

v

v

v

v

67 of 87

repr(C)

67

tag

tag

tag

tag

B

-

-

-

tag

B

size_of::<C>() == 8

#[repr(Rust)]

enum Rust {

A,

B(u8),

}

#[repr(C)]

enum C {

A,

B(u8),

}

size_of::<Rust>() == 2

68 of 87

repr(C)

68

#[repr(Rust)]

enum Rust {

A,

B(std::num::NonZeroU8),

}

#[repr(C)]

enum C {

A,

B(std::num::NonZeroU8),

}

tag

tag

tag

tag

B

-

-

-

B

size_of::<Rust>() == 1

size_of::<C>() == 8

69 of 87

FFI

  • repr(Rust) > repr(C)
  • unstable!
  • &str, Vec, HashMap, (_, _, _) — repr(Rust)

69

70 of 87

repr(C)

70

let bytes = vec![1,2,3];

struct Vec<u8> {

cap: usize,

data: *mut u8,

len: usize,

}

cap: 4

data

len: 3

1

2

3

-

?

71 of 87

repr(C)

71

let bytes = Vec {

data: std::alloc(layout),

len: 3,

cap: 3,

};

bytes[0] = 1;

bytes[1] = 2;

bytes[2] = 3;

// drop

std::dealloc(bytes.data, layout);

72 of 87

repr(C)

72

app

plugin

alloc_v1

alloc_v2

Vec

73 of 87

repr(C)

73

app

plugin

MyVec

alloc

74 of 87

#[global_allocator]

74

struct DynGlobAlloc {};

impl GlobalAlloc for DynGlobAlloc {

unsafe fn alloc(...) -> *mut u8 {

my_alloc(...) // extern “C”

}

unsafe fn dealloc(...) {

my_dealloc(...); // extern “C”

}

}

#[global_allocator]

static ALLOCATOR: DynGlobAlloc = DynGlobAlloc {};

75 of 87

arena allocation

75

extern “C” {

fn temp_alloc(layout: Layout) -> *mut u8;

fn temp_alloc_mark() -> usize;

fn temp_alloc_truncate(mark: usize);

}

let mark = temp_alloc_mark();

let data_in_temp_allocator = plugin_function();

let data = copy_to_heap(data_in_temp_allocator);

temp_alloc_truncate(mark);

76 of 87

virtual table

76

#[repr(C)]

struct MyVec<T> {

cap: usize,

data: *mut u8,

len: usize,

vtable: MyVecVTable,

}

#[repr(C)]

struct MyVecVTable {

alloc: extern “C” fn (...),

realloc: extern “C” fn (...),

dealloc: extern “C” fn (...),

drop_element: Option<extern “C” fn (*mut ())>,

}

77 of 87

virtual table

77

#[repr(C)]

struct MyVec<T> {

cap: usize,

data: *mut u8,

len: usize,

vtable: MyVecVTable,

}

#[repr(C)]

struct MyVecVTable {

allocator_fn: extern “C” fn (

old_data: *mut u8, old_len: usize, new_len: usize,

) -> *mut u8,

drop_element: Option<extern “C” fn (*mut ())>,

}

78 of 87

FFI

  • repr(Rust) > repr(C)
  • unstable!
  • &str, Vec, HashMap, (_, _, _) — repr(Rust)
  • crates.io/crates/stabby - enum optimizations + more
  • Fn(T) -> R ???

78

79 of 87

dyn Fn

79

pub fn register_command(

name: &str,

callback: impl Fn(&[&str]) -> String,

);

let context = PluginStruct::new();

register_command(

“foo”,

move |args| {

do_something_fun(args, &context)

}

);

plugin-sdk/src/lib.rs

plugin/src/lib.rs

80 of 87

trampoline

80

#[repr(C)]

struct PluginCommandHandler {

data: *mut (),

callback: extern “C” fn(

data: *const (),

args: FfiSafeSlice<FfiSafeStr>,

) -> FfiSafeString,

drop_cb: extern “C” fn(data: *mut ()),

}

81 of 87

trampoline

81

pub fn register_command(name: &str, callback: F)

where

F: Fn(&[&str]) -> String,

{

let closure = Box::new(callback);

let handler = PluginCommandHandler {

data: Box::into_raw(closure),

callback: trampoline::<F>,

drop: drop_cb::<F>(),

};

unsafe {

ffi_register_command(FfiSafeStr::from(name),

handler);

}

}

82 of 87

trampoline

82

extern “C” fn trampoline<F>(

data: *const (),

args: FfiSafeSlice<FfiSafeStr>,

) -> FfiSafeString,

where

F: Fn(&[&str]) -> String,

{

let args = args.to_vec_of_str();

let closure = unsafe { &*(data as *const F) };

let result = (closure)(&args);

FfiSafeString::from(result)

}

83 of 87

trampoline

83

extern “C” fn drop<F>(data: *mut ()) {

_ = unsafe {

Box::from_raw(data as *mut F)

}

}

impl Drop for PluginCommandHandler {

fn drop(&mut self) {

unsafe { self.drop_cb(self.data) }

}

}

84 of 87

trampoline

84

pub fn handler_call(

handler: &PluginCommandHandler,

args: &[&str],

) -> String

{

let args = FfiSafeSlice::from_str_slice(args);

let result = unsafe {

(handler.callback)(handler.data, args)

};

result.into_string()

}

85 of 87

FFI

  • внутри repr(C) и extern “C”
  • динамическую память передаём через
    • vtable
    • кастомный аллокатор локальный/глобальный
  • снаружи юзабельный и безопасный API
  • crates.io/crates/stabby

85

86 of 87

О чём ещё стоит знать

  • extern “C-unwind”, catch_unwind
  • static constructors, .init_array, static_init
  • дизайн API
  • версионирование API
  • wasm

86

87 of 87

Итог

  • plugins in Rust = dynamic libraries
  • extern “C”, repr(C), cdylib
  • libloading/crabby

87