Uzyskanie adresu pamięci struktury

0

W jaki sposób prawidłowo uzyskać adres w pamięci operacyjnej, pod którym zapisany jest obiekt?

Ja właśnie robię próby z manipulacją na obiektach (klonowanie, pożyczanie, przenoszenie własności itp) i paru rzeczy nie rozumiem.

Znalazłem dwa sposoby uzyskania tego adresu: https://stackoverflow.com/questions/35882994/is-there-any-way-to-get-the-address-of-a-struct-in-rust

Adres to chciałbym wyświetlać jako numer, który jednoznacznie określa dany obiekt. Przede wszystkim chodzi o to, żeby między innymi wypisać każdy przypadek utworzenia i zniszczenia obiektu. Ten numer jednoznacznie powinien wskazać, jest jest to jeden i ten sam obiekt.

Ostatecznie mam taki program:

struct Book
{
    title: String,
}

impl Book
{
    pub fn set_title_borrow(&mut self, tit : &String)
    {
        self.title = tit.clone();
    }

    pub fn set_title_move(&mut self, tit : String)
    {
        self.title = tit;
    }
    
    pub fn addr1(&self) -> usize
    {
        return (self as * const _) as usize;
    }

    pub fn addr2(&self) -> usize
    {
        return std::ptr::addr_of!(self) as usize;
    }

    pub fn new(name: String) -> Self {
        let book: Book = Book {title: name};

        println!("* Tworzenie 1        {} {} {}", book.addr1(), book.addr2(), book.title);
        book
    }
    
    pub fn printaddr(&self)
    {
        println!("  Adres              {} {} {}", self.addr1(), self.addr2(), self.title);
    }
}

impl Drop for Book {
    fn drop(&mut self) {
        println!("# Niszczenie         {} {} {}", self.addr1(), self.addr2(), self.title);
    }
}

impl Clone for Book {
    fn clone(& self) -> Book {
        println!("* Klonowanie       z {} {} {}", self.addr1(), self.addr2(), self.title);
        
        let mut t = self.title.clone();
        t.push_str(" kopia");

        let cl = Book {title: t};
        
        println!("                  do {} {} {}", cl.addr1(), cl.addr2(), cl.title);
        cl
    }
}


fn create_book_and_return_it_with_ownership() -> Book
{
    let book: Book = Book {title: String::from("Nowy")};

    println!("* create_book_and_   {} {} {}", book.addr1(), book.addr2(), book.title);
    book
}

fn take_ownership(book: Book) -> Book
{
    println!("  take_owner stary   {} {} {}", book.addr1(), book.addr2(), book.title);
    let x = book.clone();
    println!("  take_owner nowy    {} {} {}", x.addr1(), x.addr2(), x.title);
    return x;
}

fn borrow_test(book: &Book)
{
    println!("  borrow_test        {} {} {}", book.addr1(), book.addr2(), book.title);
}


fn main()
{
    let book3 = Book::new(String::from("Trzeci"));
    book3.printaddr();
    let mut book = create_book_and_return_it_with_ownership();
    book.printaddr();
    book.set_title_move(String::from("Czwarty"));
    book.printaddr();
    let tit = String::from("Piaty");
    book.set_title_borrow(&tit);
    book.printaddr();
    book.title = String::from("Pierwszy");
    book.printaddr();
    book = take_ownership(book);
    book.printaddr();
    borrow_test(&book);
    book.printaddr();
    let mut book2 = book.clone();
    book.printaddr();
    book2.printaddr();
    book2.title = String::from("Drugi");
    book.printaddr();
    book2.printaddr();
    book.printaddr();
    book2.printaddr();
}

Uzyskałem taki wynik, ponumerowałem linie dla czytelności:

1.  * Tworzenie 1        140736324356480 140736324356432 Trzeci
2.    Adres              140736324356816 140736324356496 Trzeci
3.  * create_book_and_   140736324356464 140736324356416 Nowy
4.    Adres              140736324356864 140736324356496 Nowy
5.    Adres              140736324356864 140736324356496 Czwarty
6.    Adres              140736324356864 140736324356496 Piaty
7.    Adres              140736324356864 140736324356496 Pierwszy
8.    take_owner stary   140736324356992 140736324356144 Pierwszy
9.  * Klonowanie       z 140736324356992 140736324355408 Pierwszy
10.                   do 140736324355696 140736324355408 Pierwszy kopia
11.   take_owner nowy    140736324356392 140736324356144 Pierwszy kopia
12. # Niszczenie         140736324356992 140736324355808 Pierwszy
13.   Adres              140736324356864 140736324356496 Pierwszy kopia
14.   borrow_test        140736324356864 140736324356496 Pierwszy kopia
15.   Adres              140736324356864 140736324356496 Pierwszy kopia
16. * Klonowanie       z 140736324356864 140736324356048 Pierwszy kopia
17.                   do 140736324356336 140736324356048 Pierwszy kopia kopia
18.   Adres              140736324356864 140736324356496 Pierwszy kopia
19.   Adres              140736324357024 140736324356496 Pierwszy kopia kopia
20.   Adres              140736324356864 140736324356496 Pierwszy kopia
21.   Adres              140736324357024 140736324356496 Drugi
22.   Adres              140736324356864 140736324356496 Pierwszy kopia
23.   Adres              140736324357024 140736324356496 Drugi
24. # Niszczenie         140736324357024 140736324356448 Drugi
25. # Niszczenie         140736324356864 140736324356448 Pierwszy kopia
26. # Niszczenie         140736324356816 140736324356448 Trzeci

Nie rozumiem następujących rzeczy:

  1. Dlaczego adres w liniach 1 i 2 się różni, mimo, że to jeden i ten sam obiekt? Tak samo linie 3 i 4.
  2. Dlaczego drugi adres w liniach 4, 5, 6 jest ten sam, mimo, że to są trzy różne obiekty?
  3. Dlaczego linie 7 i 8 mają różny adres, mimo, że to jeden i ten sam obiekt.

Wygląda na to, że adres uzyskany poleceniem (self as * const _) zmienia się za każdym razem, gdy obiekt wchodzi lub wychodzi z funkcji (mimo, że faktycznie nie następuje kopiowanie obiektu), a adres uzyskany funkcją std::ptr::addr_of!(self) może być taki sam dla kilku obiektów.

W jaki sposób prawidłowo uzyskać adres pamięci dla danego obiektu? Może pierwsza metoda źle podaje adres, bo zmienia się wtedy, kiedy faktycznie pozostaje bez zmian.

2

Pomijam fakt, że kod wygląda jak Java XD

Mogę bredzić, ale w Rust nie można myśleć jak w "obiektowce". Wartości w rust są przenoszone "moved" I nie mają "jednego miejsca".

W teoim wypadku natomiast jak odpalisz program w trybie release, a nie debug, to pewnie dostaniesz te same adresy. Ma to związek z optymalizacja robioną przez kompilator.

Pisząc to, zdałem sobie sprawę jak mało wiem o tym języku, w którym już długo pisze...

1

Takie spaghetti, że się czytać nie chce. Funkcja addr1 to jest sposób, który bym zastosował, referencję rzutuję na wskaźnik, a potem na usize jak chcesz mieć taki typ… Uprość przypadek może…

0
Dregorio napisał(a):

Pomijam fakt, że kod wygląda jak Java XD

Mogę bredzić, ale w Rust nie można myśleć jak w "obiektowce". Wartości w rust są przenoszone "moved" I nie mają "jednego miejsca".

Mam doświadczenie w C# i Java i obiektowy sposób programowania jest dla mnie czymś naturalnym.

elwis napisał(a):

Takie spaghetti, że się czytać nie chce. Funkcja addr1 to jest sposób, który bym zastosował, referencję rzutuję na wskaźnik, a potem na usize jak chcesz mieć taki typ… Uprość przypadek może…

W takim razie skróciłem i opisałem swój kod:

// Struktura przechowująca dane
struct Book
{
    title: String,
}


impl Book
{
    // Pobranie adresu jako liczby - sposób 1
    // według https://stackoverflow.com/questions/35882994/is-there-any-way-to-get-the-address-of-a-struct-in-rust
    pub fn addr1(&self) -> usize {
        return (self as * const _) as usize;
    }

    // Pobranie adresu jako liczby - sposób 2
    pub fn addr2(&self) -> usize {
        return std::ptr::addr_of!(self) as usize;
    }
    
    // W Rust nie ma konstruktorów, ta funkcja jest substytutem konstruktora
    pub fn new(name: String) -> Self {
        let book: Book = Book {title: name};

        println!("* Tworzenie          {} {} {}", book.addr1(), book.addr2(), book.title);
        book
    }

    // Wypisanie adresu obiektu - oba sposoby
    pub fn printaddr(&self) {
        println!("  Adres              {} {} {}", self.addr1(), self.addr2(), self.title);
    }
}

// Implementacja destruktora obiektu według https://doc.rust-lang.org/reference/destructors.html
impl Drop for Book {
    fn drop(&mut self) {
        println!("# Niszczenie         {} {} {}", self.addr1(), self.addr2(), self.title);
    }
}


// Funkcja ze zmianą właściciela obiektu, wypisuje adres przekazanego obiektu
fn ownership_test(book: Book)
{
    println!("  przekazanie        {} {} {}", book.addr1(), book.addr2(), book.title);
}

// Funkcja z pożyczaniem obiektu, wypisuje adres przekazanego obiektu
fn borrow_test(book: &Book)
{
    println!("  Pozyczanie         {} {} {}", book.addr1(), book.addr2(), book.title);
}


fn main()
{
    let book1 = Book::new(String::from("Pierwszy"));
    let book2 = Book::new(String::from("Drugi"));

    book1.printaddr();
    book2.printaddr();
    
    borrow_test(&book1);

    book1.printaddr();

    ownership_test(book1);

    book2.printaddr();
}
Dregorio napisał(a):

W teoim wypadku natomiast jak odpalisz program w trybie release, a nie debug, to pewnie dostaniesz te same adresy. Ma to związek z optymalizacja robioną przez kompilator.

Pisząc to, zdałem sobie sprawę jak mało wiem o tym języku, w którym już długo pisze...

Wynik w trybie "debug":

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.18s
* Tworzenie          140730438889488 140730438889440 Pierwszy
* Tworzenie          140730438889488 140730438889440 Drugi
  Adres              140730438889824 140730438889504 Pierwszy
  Adres              140730438889872 140730438889504 Drugi
  Pozyczanie         140730438889824 140730438889504 Pierwszy
  Adres              140730438889824 140730438889504 Pierwszy
  przekazanie        140730438889920 140730438889472 Pierwszy
# Niszczenie         140730438889920 140730438889136 Pierwszy
  Adres              140730438889872 140730438889504 Drugi
# Niszczenie         140730438889872 140730438889456 Drugi

Wynik w trybie "release":

    Finished `release` profile [optimized] target(s) in 0.15s
* Tworzenie          140728200876032 140728200875936 Pierwszy
* Tworzenie          140728200876032 140728200875936 Drugi
  Adres              140728200876096 140728200875936 Pierwszy
  Adres              140728200876064 140728200875936 Drugi
  Pozyczanie         140728200876096 140728200875936 Pierwszy
  Adres              140728200876096 140728200875936 Pierwszy
  przekazanie        140728200876032 140728200875936 Pierwszy
# Niszczenie         140728200876032 140728200875936 Pierwszy
  Adres              140728200876064 140728200875936 Drugi
# Niszczenie         140728200876064 140728200875936 Drugi

Wniosek jest taki, że jak obiekt wchodzi lub wychodzi z funkcji z przekazaniem właściciela, czyli bez &, to zmienia adres podawany przez addr1, natomiast dwa obiekty mogą mieć ten sam adres podany funkcją addr2.

Czy dobrze rozumiem, że samo przekazywanie obiektu wraz ze zmianą właściciela powoduje przemieszczenie obiektu w pamięci, co skutkuje zmianą adresu wskaźnika?

2
andrzejlisek napisał(a):

Czy dobrze rozumiem, że samo przekazywanie obiektu wraz ze zmianą właściciela powoduje przemieszczenie obiektu w pamięci, co skutkuje zmianą adresu wskaźnika?

Dobrze rozumiesz. Adresy nie powinny być statyczne bo to ułatwia ich analizę i zwiększa powierzchnię ataku przez potencjalnego agresora. Do tego im więcej przemieszczeń w pamięci generowanych przez program tym trudniej wykonać zewnętrzny atak typu rowhammer przed którym nie znamy żadnego sensownego zabezpieczenia :(

1

No dokładnie tak jest, że w konstruktorze tworzysz obiekt, którego adres pobierasz, a potem przenosisz wraz ze zwróceniem wartości. Gdybyś chciał tego uniknąć, mógłbyś alokować na stercie, np używając Box<Book>.

Jeśli chodzi o funkcję addr2 to ona podaje adres w pamięci w którym znajduje się wskaźnik (parametr), dlatego skoro funkcje są wywoływane w tej samej funkcji to logiczne, że pierwszy parametr jest pod tym samym adresem. :) std::ptr::addr_of!(self) daje taki sam wynik jak&self as *const _. Tak więc to raczej musiałoby być std::ptr::addr_of!(*self).

0

W takim razie, w jakiej postaci przekazywany jest obiekt do funkcji z przekazaniem właściciela?

Funkcje borrow_test i ownership_test wydają się być takie same za wyjątkiem faktu, że ta pierwsza tylko udostępnia book1 do skorzystania, a przy wywoływaniu ownership_test następuje podarowanie obiektu, że to funkcja ownership_test jest nowym właścicielem.

W przypadku borrow_test następuje przekazanie obiektu przez referencję (wskaźnik), więc normalne jest, że adres odczytany zarówno wewnątrz funkcji, jak i poza funkcją, jest taki sam.

Natomiast, przy wywołaniu ownership_test jest przekazanie parametru przez referencję, czy przez wartość? Przy tak małych strukturach to nie ma znaczenia wydajnościowego, ale przy gigantycznej macierzy może to mieć znaczenie. Czy można powiedzieć, że ownership_test przekazuje parametr jako wartość i stąd wynika inny adres? Jeżeli tak, to wtedy jest zrozumiałem, że w momencie wywołania ownership_test, patrząc od zewnątrz funkcji, zawartość pamięci struktury jest przekazywana jako parametr i ta pamięć jest uwalniana, a patrząc od wewnątrz funkcji, ona nie dostaje wskaźnika na strukturę, tylko dostaje zawartość struktury, więc musi na nowo zarezerwować pamięć, żeby gdzieś zachować dane z parametru i stąd jest nowy adres.

Całe zagadnienie bierze się z tego, że na początek nauki rusta chciałem mieć jakiś sposób, żeby móc śledzić istnienie obiektu od jego utworzenia do jego zniszczenia. W przypadku języka C++, każde tworzenie i każde niszczenie wykonuje programista i kompilator w ogóle nie kontroluje prawidłowości tych czynności, a także nie gwarantuje zniszczenia obiektu w przypadku zgubienia wskaźnika na obiekt. Wydawało się, że najprostszą rzeczą jest pozyskanie adresu w pamięci, zakładając, że podczas życia obiektu, nie ma niekontrolowanego uwolnienia i ponownego zarezerwowania pamięci tego obiektu.

2

Rust (podobnie z resztą jak Java) zawsze przekazuje przez wartość. Nie da się tego zmienić. Po prostu czasem przekazaną wartością jest referencja (podobnie jak w Javie). Zasadniczą różnicą między Javą a Rustem tutaj jest to, że w Ruscie to programista decyduje w jakiej formie jest to przekazane (oraz decyduje o ew. mutowalności przekazanej referencji).

więc musi na nowo zarezerwować pamięć, żeby gdzieś zachować dane z parametru i stąd jest nowy adres

Tak, ale nie do końca, bo kompilator jak najbardziej może usunąć tę kopię i przekazać obiekt "as is", bo ze względu na zasady ownership, mamy gwarancję, że nikt nie "zepsuje" nam tego obiektu jak jesteśmy jego właścicielem.

mieć jakiś sposób, żeby móc śledzić istnienie obiektu od jego utworzenia do jego zniszczenia

Zasadniczym pytaniem tutaj jest - po co? Jaki konkretnie problem chcesz tutaj rozwiązać.

0

Rust (podobnie z resztą jak Java) zawsze przekazuje przez wartość. Nie da się tego zmienić. Po prostu czasem przekazaną wartością jest referencja (podobnie jak w Javie).

To chyba jedno drugiemu zaprzecza. W przypadku Java, jak parametr jest wartością prymitywną, to zawsze przekazuje przez wartość (nawet nie da się w prosty sposób przekazać przez referencję, przeciwieństwie do C++ i C#), ale jak parametr jest typu jakiejś klasy bądź kolekcji, to parametr zawsze przekazywany jest przez referencję, bo przy wywołaniu funkcji nie jest tworzona kopia wartości, tylko operuje bezpośrednio na jednym i tym samym egzemplarzu obiektu. Także tutaj, nie da się zmienić, żeby przekazać obiekt przez wartość w sensie, że funkcja operuje na kopii instancji klasy.

Zasadniczą różnicą między Javą a Rustem tutaj jest to, że w Ruscie to programista decyduje w jakiej formie jest to przekazane (oraz decyduje o ew. mutowalności przekazanej referencji).

Rozumiem, że sposoby są następujące w przypadku struktur:

  1. Pożyczenie - przekazuje przez referencję.
  2. Oddanie własności - przekazuje przez wartość.
  3. Typy podstawowe zawsze przekazuje przez wartość.

Zasadniczym pytaniem tutaj jest - po co? Jaki konkretnie problem chcesz tutaj rozwiązać.

Problemem biznesowy, to uzyskanie unikatowego identyfikatora do celów edukacyjno-testowych. Już dawno stwierdziłem, że w odróżnieniu od Javy czy C++, nie ma możliwości tworzenia zmiennych globalnych, a więc w Rust nie da się zrobić czegoś takiego:

int globalId = 0;

int getGlobalId()
{
    globalId++;
    return globalId;
}

class SomeClass
{
public:
    int id;
    SomeClass()
    {
        id = getGlobalId();
    }
};

int main(void)
{
    SomeClass obj1;
    SomeClass obj2;
    SomeClass * obj3 = new SomeClass();
    SomeClass * obj4 = new SomeClass();
    std::cout << obj1.id << std::endl;
    std::cout << obj2.id << std::endl;
    std::cout << obj3->id << std::endl;
    std::cout << obj4->id << std::endl;
    delete obj3;
    delete obj4;
    return 0;
}

Przyjmuję do wiadomości, że nie ma takich wskaźników w pamięci podobnych do tych w C++. Podobnie Java to w ogóle nie ma żadnych wskaźników. Pozostaje jedynie próba z obiektami statycznymi i singletonem. Potem pozostaje odpuścić, próby uzyskania adresu w pamięci na ogół i tak, nie są do niczego potrzebne.

2
andrzejlisek napisał(a):

problemem biznesowy, to uzyskanie unikatowego identyfikatora do celów edukacyjno-testowych. Już dawno stwierdziłem, że w odróżnieniu od Javy czy C++, nie ma możliwości tworzenia zmiennych globalnych, a więc w Rust nie da się zrobić czegoś takiego:

można zrobić static i będziesz miał zmienną globalną, jeśli naprawdę potrzebujesz (z tym, że mutowalne statiki to jest operacja unsafe). W przykładzie wyżej nie widzę, żeby była potrzeba. Pomijając już, że to wszystko w main jest (więc w tym konkretnym przypadku wystarczyłaby zmienna lokalna w main), to nawet wyobrażając sobie, że będzie z tego większa apka i obiekty tworzone będą w różnych miejscach, to dlaczego SomeClass ma w ogóle sam sobie ustawiać id? Moim zdaniem lepiej to opakować w fabrykę. Tylko pytanie - czy fabryka powinna mieć dostęp do tej zmiennej globalnej? Też niekoniecznie. Może zależy od stylu programowania, ale ja zwykle wolę zakładać, że wszystko jest w strukturach i nawet do tych "globalnych" rzeczy robię strukturę App albo coś podobnego np.

struct App {
    last_id: usize
}

impl App {
    fn global_id(&mut self) -> usize {
        self.last_id += 1;
        self.last_id 
    }
}

#[derive(Debug)]
struct SomeStruct {
    id: usize
}

fn main() {
    println!("Hello, world!");
    let mut app = App { last_id: 0 };
    for i in 0..5 {
        let a = SomeStruct { id: app.global_id() };
        println!("{:?}", a);
    }

}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=ab7403f11360cbf2823086f8e841e7e6
i do tego App można potem dodać dane "globalne" takie jak lista obiektów, funkcje-fabryki do ich tworzenia itp. Tak, żeby te dane "globalne" były jednak trzymane w jakiejś konkretnej strukturze.

No ale jednak dałoby się zrobić na static, mając zwykłe zmienne globalne:

static mut last_id: usize = 0;

fn global_id() -> usize {
    unsafe {
        last_id += 1;
        last_id 
    }
}


#[derive(Debug)]
struct SomeStruct {
    id: usize
}

fn main() {
    println!("Hello, world!");
    for i in 0..5 {
        let a = SomeStruct { id: global_id() };
        println!("{:?}", a);
    }

}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=b98f5b9d3a0fc88cf8dc04d0e3f9d0ad

2
andrzejlisek napisał(a):

To chyba jedno drugiemu zaprzecza. W przypadku Java, jak parametr jest wartością prymitywną, to zawsze przekazuje przez wartość (nawet nie da się w prosty sposób przekazać przez referencję, przeciwieństwie do C++ i C#), ale jak parametr jest typu jakiejś klasy bądź kolekcji, to parametr zawsze przekazywany jest przez referencję, bo przy wywołaniu funkcji nie jest tworzona kopia wartości, tylko operuje bezpośrednio na jednym i tym samym egzemplarzu obiektu. Także tutaj, nie da się zmienić, żeby przekazać obiekt przez wartość w sensie, że funkcja operuje na kopii instancji klasy.

Java nie ma referencji według klasycznej definicji. Javowe referencje na klasę to zwykłe wskaźniki z taką samą semantyką jak wskaźniki z C (można je nullować, można je przepisać do innej zmiennej, można je nadpisywać). Java po prostu nie pozwala na przekazanie obiektu przez wartość, bo język nie pozwala na dobranie się do obiektu inaczej niż przez referencję.

W takim C++ masz string i string*. Java nie pozwala na używanie typu string tylko masz String, który jest odpowiednikiem string* z Cpp. Samego String bez wskaźnika nie sposób doświadczyć, bo:

  • wszystkie operacje dzieją się przez referencję
  • nie ma semantyki kopiowania ani wyłuskania
  • obiekty robią się magicznie dzięki operatorowi new String, który zwraca referencję na obiekt

Podobnie Java to w ogóle nie ma żadnych wskaźników

Java to głównie wskażniki. Po prostu ludzie o tym tak nie myślą, bo nie ma arytmetyki jak i nie ma alternatywy w postaci wartości trzymanych w miejscu.

1

Z odrobiną samozaparcia (i głupoty) można coś takiego zrobić:

use std::sync::atomic::{AtomicU64, Ordering};

static COUNTER: AtomicU64 = AtomicU64::new(0);

struct SomeStruct(u64);

impl SomeStruct {
  fn new() -> Self { SomeStruct(COUNTER.fetch_add(1, Ordering::Relaxed)) }
}

fn main() {
    let a = SomeStruct::new();
    let b = SomeStruct::new();
    let c = Box::new(SomeStruct::new());
    let d = Box::new(SomeStruct::new());

    println!("{}", a.0);
    println!("{}", b.0);
    println!("{}", c.0);
    println!("{}", d.0);
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=d836ecba7099f4c0999943b88e7fd7a4

Jednakowoż użyteczność czegoś takiego (a zwłaszcza testowalność) jest IMHO znikoma.

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.