#Mutable struct field via reference?

1 messages · Page 1 of 1 (latest)

dapper palm
#

I am passing player to controller by reference like so:


pub const Game = struct {
    const Self = @This();

    allocator: std.mem.Allocator,

    camera: rl.Camera3D,
    player: Player,
    asteroid: Asteroid,
    controller: Controller,

    pub fn init(allocator: std.mem.Allocator) !Self {
        var player = try Player.init(allocator);

        return Self{
            .allocator = allocator,
            .camera = initCamera(),
            .player = player,
            .asteroid = Asteroid.init(),
            .controller = Controller.init(&player),
        };
    }

But I am unable to modify some of player's fields namely it's velocity:

const std = @import("std");
const rl = @import("raylib");

pub const Player = struct {
    const Self = @This();

    allocator: std.mem.Allocator,

    model: rl.Model,
    position: rl.Vector3,
    rotation: rl.Vector3,
    velocity: rl.Vector3,

    pub fn init(allocator: std.mem.Allocator) !Self {
        const mesh = try initMesh(allocator);

        return Self{
            .allocator = allocator,
            .model = rl.LoadModelFromMesh(mesh),
            .position = rl.Vector3.zero(),
            .rotation = rl.Vector3.zero(),
            .velocity = rl.Vector3.zero(),
        };
    }

In controller:

#

pub const Controller = struct {
    const Self = @This();

    player: *Player,

    pub fn init(player: *Player) Self {
        return Self{
            .player = player,
        };
    }

    pub fn update(self: Self) void {
         self.player.*.velocity.* = rl.Vector3.new(0.11, 0.01, 0.0);
         // self.player.*.position = self.player.position.add(rl.Vector3.new(0.1, 0.0, 0.01));
        // self.player.velocity = self.player.velocity.scale(friction);

        if (rl.IsKeyDown(rl.KeyboardKey.KEY_Z)) {
            const rotation = rl.MatrixRotateXYZ(self.player.rotation);
            const direction = rl.Vector3.new(rotation.m8, rotation.m9, rotation.m10).normalize();

            self.player.velocity = self.player.velocity.add(direction.scale(acceleration));
        }

The comments are where I've tried different stuff out

#

When I mutate player in player, it works fine, just externally it doesn't. Can't seem to figure out why

errant lantern
# dapper palm ```zig pub const Controller = struct { const Self = @This(); player: *...

It's a lifetime issue.
In the game initialising function&player is a pointer to var player which is a local variable.
The lifetime of a local variable is the scope where that variable is declared.
So basically you have a use after free, a UAF; the game instance holds onto this pointer but the location on the stack where the player local variable is, is reused by future function calls.

dapper palm
#

you're right

#

I haven't slept man

#

Idk how I didn't catch that

errant lantern
#

This is why I routinely give the advice that whenever you type an ampersand you should have already convinced yourself that this &, that this lifetime, is the length that you need it to be

dapper palm
#

It looks like the compiler is doing some weird escape analysis?

dapper palm
native violet
#

I think even C compilers disallow this now

dapper palm
#

That's why I'm even more confused

#

Surely the reference would go out of scope?

errant lantern
#

The stack isn't ever freed it's reused.
This kind of bug manifests in the way you've discovered where it only actually matters at all you only actually notice if a future function calls local variable happens to overwrite the same location that player was in

native violet
#

it is there but it’s just a reference to a stack frame for your function, upon return and another function being called it’ll be overwritten most likely* (new to zig I may be missing something)

errant lantern
# dapper palm The reference works for inside game btw

This means that this doesn't really tell you anything.
You just simply have to be sure that the lifetime you need is the lifetime you've got and you have to be meticulous and aware about that and that's not as hard as it may sound because you just have to think about it whenever you type an ampersand.
Which when you get familiarity with this level of control comes quite naturally to you.

dapper palm
#

Usually it would have segfaulted

errant lantern
native violet
#

I meant there could be zig specific behavior, since goal and has invisible behavior regarding this

errant lantern
#

If the pointer points to a place on the stack say 4,000 bytes into the stack deep in a bunch of function calls you then return out of most of them and then keep using that pointer chances are it will be valid for some time -- at least until you use a similar amount of stack; 4000 bytes again and overwrite it.

native violet
dapper palm
#

Shouldn't the init stack frame get popped?

#

I'm definitely calling stuff after it

native violet
#

DrawLine() calls DrawSquare(), now imagine if you pass locals of DrawSquare to a function and then return from it (and eventually another stack frame ends up there)

dapper palm
#

That's why it's confusing

native violet
#

popped doesn’t overwrite the data

dapper palm
#

It works only in game.zig but not in controller.zig

#

They both have access to the same reference supposedly

errant lantern
# dapper palm Usually it would have segfaulted

Right it will only segful if the pointer itself points to a place in memory that you're not allowed to access anymore but of course the stack in entirety remains valid for the entire life of the program none of it is ever freed - just that a region of it is being used for a particular function call

native violet
#

at the time you’re passing the reference, all is ok. it’s only overwritten later by subsequent function calls

dapper palm
#

That's what confused me

#

Some escape analysis mechanic that doesn't get triggered before hand

native violet
#

nothing like that, just regular stack behavior

dapper palm
#

Ah I misunderstood

#

Shouldn't controller and game hold the same stack reference though?

errant lantern
#

Indeed there isn't any magic here really it's just that the stack works by having a offset into the stack which represents the start of the current functions frame the region I spoke about before and that offset changes as you enter a function or leave a function just simply by adding or subtracting to it

dapper palm
#

That's how stack pointers work in zig?

#

Alright got it

errant lantern
# dapper palm Shouldn't controller and game hold the same stack reference though?

Controller holds a pointer to this local variable however game does not hold a pointer to it it holds a copy of it because the type of the field is a player value and because of the general philosophy of zinc being that of c rather than c++ values generally get copied and there are no move semantics for example anything like that it's just it's more what you see is what you get

errant lantern
# dapper palm That's how stack pointers work in zig?

It's important to realise that this is how the stack works at an assembly level and languages like zig Odin c even just use that they just expose that to you basically they don't try and put anything in between

dapper palm
#

Difference is, the stack and heap pointers are homogenous

#

Not relative to the frame

native violet
#

if you do this in C you’ll crash as well

dapper palm
#

That's the difference

errant lantern
#

I'm not quite sure you mean but see will exhibit very similar behavior for exactly the same reason if you do stuff like this in C

dapper palm
#

Here I had a dangling pointer for the entire lifetime of the program

#

No seg fault

#

Working completely fine

#

Doing everything

errant lantern
native violet
#

that’s just the sequence of function calls

#

if you call A -> B -> C and pass a reference like that through each function you won’t crash

errant lantern
#

Right it's entirely dependent on the usage of the stack after the scope ends

#

This becomes much more apparent if you enable optimization because the optimizer is allowed to assume that you never will do this and therefore for example you will probably notice much more quickly you're probably crash much more quickly

#

The variable player might not even be in the memory at all it might just be put in a register and therefore never have a valid address now of course this is dependent on it fitting in a register in the first place which it may not here but you get the point that I'm making

native violet
#

but if you call A -> B, return, A -> B and then try to access some reference from B (the first invocation), the data will have been overwritten

dapper palm
#

I usually wouldn't stack allocate an item with a lifetime larger than it's scope lmao

native violet
#

I think learning a bit of asm will help you a lot

dapper palm
#

I literally haven't slept and I was frustrated hahaha

dapper palm
#

A lot of garbage collection stuff, stack evictions

#

I'm not necessarily uncomfortable with this stuff, I'm just not used to zig's semantics

#

I started a couple hours ago lmao

native violet
#

I don’t know how lisps runtime works so it’s possible you’d never implement function stacks this way

#

also lisp implementations usually have tail call optimizations so would be even further from what happens on a CPU

dapper palm
native violet
#

Moeed: Unrelated, but you might want to store your entities in a heap allocated MultiArrayList instead (and just do more things on the heap in general). Game/entities should be sparsely created/destroyed so the cost of heap allocating is negligible

#

implement a control flow flattened obfuscation, you’ll discover many :P

dapper palm
#

Just finished CPS transform

#

Closure conversion

#

Then I'm gonna write a small instruction set for bytecode

#

Then jit it to x86

native violet
#

don't forget the basics though

dapper palm
#

All the large objects behind the scenes are heap allocated

native violet
#

It depends on the scope of the game. You're going to blow up the stack if you have a decent number of entities with some properties

dapper palm
#

But rn it's not necessary and it's just more memory to handle. I like to keep things simple as I prototype

native violet
#

I find stack memory to be a much bigger PITA than heap memory, but if it works for you then go ahead

dapper palm
#

There's no reason to use the stack

#

I've just been in a very low level mindset lately

native violet
#

You do use the stack in games when you need to allocate in a render loop or somewhere perf critical

#

But even then i just use a FBA with a heap fallback

dapper palm
#

Depends

#

Stack and heap are a false dichotomy 90% of the time

#

If you need large amounts of dynamic objects, I would just use arena allocator at that point

native violet
#

I did try an arena allocator first, but the performance wasn't anything to write home about. I backed it with a page allocator though, maybe rpmalloc or something would've been better

dapper palm
#

Otherwise it's very cheap

#

Just bumping a pointer

#

I think for this game, memory pooling would be enough

#

Pre-allocate 200 asteroids and just reuse them

#

All runtime allocations avoided

native violet
#

I'll try again with different allocators when i find the time. I still think FBA will be faster, but if the performance can be reasonably competitive then i'm willing to degrade it just to get rid of as much stack memory as feasible

native violet
dapper palm
#

With a free list

native violet
#

Dynamic lights

dapper palm
#

Especially if you're having to move around data to and from gpu

native violet
#

It's not that bad, just a separate render pass. The problem is having to enumerate light data before the fact or loop through entities/etc again which would be even slower

dapper palm
native violet
#

No it's just an arraylist backed by the "custom" allocator with the FBA and the fallback rpmalloc

#

Have to force resize or else memory won't be contiguous when it hits the fallback, but that rarely happens

dapper palm
native violet
#

The stdlib arraylist wouldn't even work otherwise i'm pretty sure. But it has to be contiguous anyway to avoid cache misses

errant lantern
dapper palm
dapper palm
native violet
#

The entire light data list will have to be iterated over and the order does not matter, so i'm not sure how it could get any faster than just a contiguous blob of memory

native violet
#

How does that help though. I need to access every single element with no regards to order