R
R
RoadToGamedev2021-07-28 15:42:00
Rust
RoadToGamedev, 2021-07-28 15:42:00

Rust unsafe, what are the pitfalls and how to approach C API design?

I wrote this post to find answers to my questions.
They are related to Rust and its unsafe bindings to C.
The further I go, the more painful it is for me to look at something.
Maybe from my stupidity, maybe from something else.
My goal is to build a small binding, parts of Raylib.

The first thing I will note. The bindgen tool doesn't work for me.
I don't like 2,000 lines of constants (a figure from a particularly curious Raylib binding).
Of which 1900 will never be used by me.
Maybe I don’t understand something and he doesn’t sew them somehow?
But it's the same with functions/structs.
Let me remind you again that I'm trying to bind a small part of Raylib.
I have experience developing bindings in Python and Golang from scratch.
I also used this binding in C, C++, D (a little bit).
There are many reasons why I work with this library.
From license to personal interests and experience.

At one point, I decided to think of Rust as a replacement for Go.
I like to make games, for myself, for friends, partly for business.
Looking at Rust, I really want to program on it, but ...
Unsafe poke me so that I would run.
Probably not only in Rust, but I only encountered this in Rust.

Let 's take a simple C example:
RLAPI void DrawRectangle(int posX, int posY, int width, int height, Color color);
Rust:
extern "C" {
fn DrawRectangle(pos_x: c_int, pos_y: c_int, width: c_int, height: c_int, color: Color);
}
All is well, with one amendment. If I change anything. Rust will only praise me.
In Color, I can specify &Color and send a pointer there. This is how it works in many places.
Results.. The most different. From the wrong color that the pointer set (apparently the pointer has become a color), to the working code. By specifying a reference to the Sound structure.
I got a working result. As if everything is fine. And that gets me a little.
Okay, these are the little things. We need to be a little more careful about the C call block itself.
Although how can I specify a link to the structure, but not send the structure, and this works somewhere, but not somewhere?
Well, how? Okay, let's move on ...

When we exported everything correctly. I started to wonder what a safe API to unsafe means.
After reading manuals from Rust and a couple of books. Looking at someone else's code. I completely lost my sense of reality.
In one book, the emphasis was on the fact that the initial function should control the children.
To be more precise, we must make sure that the language itself speaks to us. You can't use function B
until you call A. Sounds logical, in practice.. Something like this.

This is pseudocode, but it +- reflects reality.

// привычное api на всех языках.
fn main() {
window_init(1152,648,"Hello, world!");
set_target_fps(60);
while !window_should_close() {
begin_drawing();
clear_background(&Color{r:0,g:0,b:0,a:255});
draw_texture(&testr,10,10,&Color{r:255,g:255,b:255,a:255});
end_drawing();
}
close_window();
}

// На Rust пытаются сделать так.
fn main() {
window_init(1152,648,"Hello, world!");
set_target_fps(60);
while !window_should_close() {
begin_drawing();
clear_background(&Color{r:0,g:0,b:0,a:255});
draw_texture(&testr,10,10,&Color{r:255,g:255,b:255,a:255});
}
}


In short, the code, the function of closing the window and the end of drawing will end by itself, when leaving the visibility zone.
Like Rust, the library is trying to save us from a stupid mistake! I need to say thank you to her.
That's how they try to do it in a lot of places. I won't point fingers. True, all this is done on structures, flows, etc.
And the code looks not less, but more.
Sematics from my point of view collapses, having come from the world of SDL, it is possible to panic a little.
Binding to SDL is done more reasonably than to other things, I guess. I didn't go into it much.
Because that's not my goal.
But other people made me feel worse. I'm used to not knowing how to write code, look, analyze, understand someone else's.
I'm not sure what to do here. But let's figure out what's wrong with all these attempts to secure function closures.
Although in Go this is done via defer immediately after opening. Here they are trying to automate.
And what do we get? in 90% of cases, I need to explicitly, after the end of the drawing, produce some kind of logic.
After explicitly closing the window the same way. Save logs, run or upload something.
Since they do it, in an attempt to repeat the Drop for going out of sight. Spoils not only the readability of the code.
But also a lot more.. I'm not good at joking, but imagine the HTML syntax and stop inserting a closing tag.
It will look terrible.

Now I will go through different libraries, but many of them are similar in one way.
We check for an error, but immediately panic!

An example from the Raylib world.
window_init(1152,648,"Hello, world!");
bool = is_window_ready()

//next logic.

But in many frameworks, I see the hardcoded word panic.
As for me, this is unacceptable! We have to do something in trying.
A. Fix the problem if possible.
B. Notify the user. There is no console. So native error box.
C. Write to the log or send somewhere.

It is difficult to fit this into a method, as many try to do. Of course, you can ask for an anonymous function or something else ..
But do you understand what I mean? Sometimes simple things are not worth trying to automate out of the way. As long as you don't make a mistake.
But alas, I see a lot where just the word panic or error. Media loading, window control, sound, etc.
Not everywhere of course, but where I see. This is a failure!

Let's look at an example from binding to Raylib.
extern crate raylib;
use raylib::prelude::*;
use structopt::StructOpt;

mod options;

fn main() {
let opt = options::Opt::from_args();
let (mut rl, thread) = opt.open_window("Logo");
let (w, h) = (opt.width, opt.height);
let rust_orange = Color::new(222, 165, 132, 255);
let ray_white = Color::new(255, 255, 255, 255);

rl.set_target_fps(60);
while !rl.window_should_close() {
// Detect window close button or ESC key
let mut d = rl.begin_drawing(&thread);
d.clear_background(ray_white);
d.draw_rectangle(w / 2 - 128, h / 2 - 128, 256, 256, rust_orange);
d.draw_rectangle(w / 2 - 112, h / 2 - 112, 224, 224, ray_white);
d.draw_text("rust", w / 2 - 69, h / 2 + 18, 50, rust_orange);
d.draw_text("raylib", w / 2 - 44, h / 2 + 48, 50, rust_orange);
}
}

You probably see a working, good code.
I see MAGIC!
The first is the magic with the flow and the opening of the window. We can look at the documentation and understand something.
But it becomes more and more unclear. What for?
The other two magics lie in the fact that the window is closed and drawing is not necessary.
It's good to understand how it works. I can't understand why?
We have a bunch of modules that are clearly not related to each other and we have a bunch of different structures to manage the whole thing. On some side, I agree, it gives security to the code. But I can't read it.
What is d which gives us begin_drawing. This is no longer self-documenting code. This is a set of magic.
Or an increase in the number of variables.
Honestly, knowing less Raylib I would have thought. Wow they use multi-threaded rendering.
Or something like that. In fact, this is far from the case ...
But I really like the example with the delay in the rendering time of the frame. Seems with theard sleep in SDL.
I just look at it and I have a smile. Ah yes inventors, ah yes savvy.
In general, a normal, readable API turns into a mess.

Eh, then I looked at the source code of one game .. where there were 3k lines in one file.
50 cycles. Cycle to the stage. And I thought, what good fellows. I must be such a bad programmer.
That it is hard for me to master this example.

What is the use of automatic unloading of assets.
If 90% are unloaded are loaded in the process? I don't understand.

But in the end, I'm an idiot.
I can't master Rust unsafe because I don't understand. Do we really need these streams?
Do we really need to have automatic unloading of everything and everything everywhere?
I don't understand where 1 inflection towards C ends and another inflection towards Rust ends.
And it seems that the language is not bad and much can be done in an interesting and safe way, but where does this inflection end?

My plan is this. Wrap all calls to C with simple functions. Do a couple of checks and don't get brainwashed.
Or maybe there are good reasons to do something really like that. . complex. With streams, channels, different chips?

You tell me please! And I really don't understand.
Can't build an asset manager? Which will, on command, clean something or not clean it? Like a hash map?

You can't make code that can ONLY run on a single thread. Just in 1 function. And what is multi-threaded separately?
Do I really need 100 and 1 module per sneeze? I understand OOP. Window object, Pictures object.
But is it possible to explicitly close the window somehow? And leave the pictures next to the textures. Yes, textures, for example, cannot be loaded without opening a window. And yes, you can be wrong. But is it really worth the complexity of the logic of the entire application?
When can all this be initialized after opening, for example, even with 1 function? who is responsible for it.

There is really one more question. This is copying structures. It turns out that we copy the structure for each sneeze.
Trying to explicitly send to C. 60 frames per second in a loop. Because we pass the function by pointer to the wrap.
And there we already copy and give C. Somehow I didn’t even think about it and I don’t know how to test it in Rust.
Structures with Copy do not have Drop. Actually, it's empty. You see Rust, the truth breaks my head too much.
Help me please! Will it always be like this or will it go away when I understand it? and will I understand?
It's hard to be a solo developer, and you like something... but it kind of pushes you away.
Alas, I like Rust, but other than how to make games on it. I have nothing else to do.
And he me with his unsafe and sentences in every line. Be careful not to touch unsafe.
We are not responsible for unsafe, etc. Repels himself. Oh, sometimes it's hard without a standard. Clear and understandable.
So once again I ask, help whoever you can. I'm in terms of advice, experience, maybe links to non-trivial literature.
What are the real pitfalls? It is possible in LS.
Dictionary Rus. language won't help. I say right away. True, you can send me back to the world of Go / C / C ++, but I will remind you of this :)

Answer the question

In order to leave comments, you need to log in

1 answer(s)
V
Vasily Bannikov, 2021-07-28
@vabka

Don't fool yourself and take a ready-made safe wrapper over raylib
https://crates.io/crates/raylib
And the guide on working with unsafe is rustonomicon
unsafe itself just allows you to use raw pointers + call other unsafe functions.
A safe wrapper is when you, with the help of types and all sorts of validations, guarantee correct use.
Here's an example from the one above:

use raylib::prelude::*;

fn main() {
    let (mut rl, thread) = raylib::init()
        .size(640, 480)
        .title("Hello, World")
        .build();

    while !rl.window_should_close() {
        let mut d = rl.begin_drawing(&thread);

        d.clear_background(Color::WHITE);
        d.draw_text("Hello, world!", 12, 12, 20, Color::BLACK);
    }
}

If this is magic for you, then you need to study Rust a little deeper and look at the source.
For the future: do not write a huge footcloth of text with a bunch of questions, but write only what is directly related to the main question.
Ask other questions separately.

Didn't find what you were looking for?

Ask your question

Ask a Question

731 491 924 answers to any question