Sécurité reinterpret_cast

Pour mélanger C++ et Scala

Le problème exposé dans ce sujet a été résolu.

Bonjour à tous,

Dans le cadre d'un projet, je suis amené à mélanger du C++ et Scala. L'idée générale c'est de faire des requêtes sur des ensembles d'objet. La répartition est la suivante :

  • la construction de requêtes complexes est faite en Scala
  • Le C++ me sert de backend, c'est lui qui gère les objets et les interroge sur des propriétés primitives.

Pour identifier les objets dans le monde Scala, je n'ai rien trouvé de mieux que caster les adresses en std::uintptr_t et lorsque je veux demander quelque chose, je recaste l'entier vers un pointeur éventuellement d'un sur type du type de l'objet de base

Le code ci dessous résume l'idée générale (à la différence principale que ask_foo vient de la JVM).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
#include <cstdint>

struct A{
  int i = 0;
  virtual int foo() {return i;}
};
struct B : A{
  int foo() override {return i+2;}
};

using handle_t = std::uintptr_t;
handle_t get(B& a){
  return reinterpret_cast<handle_t>(&a);
}

void use(handle_t h){
  auto p= reinterpret_cast<A*>(h); //On cast en A*
  std::cout << p->foo() << "\n";
}

int main(int argc, char *argv[])
{
  B a;
  auto h = get(a);
  use(h);
  return 0;
}

La page de cppreference sur le sujet indique qu'on peut :

  • Faire le cast B* -> uintptr_t
  • Faire le case cast uintptr_t -> B*
  • Faire le cast B* -> A*

Est ce que pour autant fusionner les 2 dernières opérations est sans danger ? Car pour le moment, ca marche bien, mais est c'est pas juste un gros coup de chance ?

Merci ! David.

+0 -0

A priori, je ne vois pas où ça pourrait planter. Le seul risque à mes yeux est de caster ton std::uintptr_t vers un type C qui ne serait pas la classe mère de ton objet original.

A la limite, tu pourrais avoir des résultats incohérents si certaines de tes fonctions ne sont pas virtuelles.

La où j'ai le plus peur, c'est si tu souhaites manipuler des objets constants, je ne sais pas trop comment reinterpret_cast gère ce genre de truc (s'il casse le const).

A priori, je ne vois pas où ça pourrait planter. Le seul risque à mes yeux est de caster ton std::uintptr_t vers un type C qui ne serait pas la classe mère de ton objet original.

Ouais et là ca peut faire n'importe quoi. Mais c'est de ma faute et ca ne devrait pas arriver car j'ai des mécanismes en scala pour éviter ca.

A la limite, tu pourrais avoir des résultats incohérents si certaines de tes fonctions ne sont pas virtuelles.

Bah c'est pas différent de l'appel d'une fonction non virtuelle dans une situation de polymorphisme, je ne vois pas le soucis.

La où j'ai le plus peur, c'est si tu souhaites manipuler des objets constants, je ne sais pas trop comment reinterpret_cast gère ce genre de truc (s'il casse le const).

Les objets ne sont pas constants, donc le problème ne se pose pas, mais c'est vrai que je n'y avais pas pensé.

+0 -0

Si ta fonction, disons f pour être original, n'est pas virtuelle, alors si tu appelles f sur l'objet retourné par ta méthode use, tu appellera A::f, et non B::f. C'est ce que je voulais dire par "résultats incohérents". Après, comme je ne sais pas trop ce que tu veux faire, je ne sais pas si c'est vraiment un problème.

Vu que B hérite de A en premier (parce que seul), A est placé en mémoire au tout début, on peut donc sans problème prendre pour acquis que &b == static_cast<A*>(&b) à partir de là, aucune raison que ça pose problème.

germinolegrand

Enfin là c'est juste un exemple, en vrai je manipule des éléments d'AST tirés de clang/libtooling et y'a de tout (héritage sur plusieurs niveaux, héritage multiple).

j'ai aussi posé la question SO, et il ressort que :

  • reinterpret_cast ne convient pas pour un upcast
  • La conversion ptr->int->ptr n'est garantie que si on recaste vers le même type

Donc je ne sais pas quoi penser =/

+0 -0

Si t'as de l'héritage multiple, et du virtual, c'est un peu mort à mon avis. Notamment parce que t'as pas cette garantie justement que c'est la même adresse mémoire.

Dans tous les cas c'est UB. Même si ça va marcher 99% du temps (faudrait que ton A soit pas la première base d'héritage par exemple).

+1 -0

J'ai 2 questions:

  • Est-ce nécessaire de manipuler le type de base ? Ou l'inverse, n'est-il pas possible de caster vers le type de base avant transformation en intptr ? Quitte à passer par un tableau de intptr pointant sur différents type avec pour origine la même instance.
  • Le nombre de type manipuler est-il connu ? Dans l'idée de faire des matrices de fonctions de cast avec chaque fonction qui fait la conversion intptr -> type réel -> type désiré. Il faut en plus stocker un identifiant unique pour chaque type.
+0 -0

C'est en écrivant une réponse à jo_link_noir que j'ai eu une idée. Quel problème n'est pas résolu par de la magie noire à base de métaprogrammation et de template ? Aucun ! L'idée étant d'éviter de se palanquer à la main des kilomètre de if pour caster vers le bon type.

Le résultat :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#include <boost/mpl/at.hpp>
#include <boost/mpl/for_each.hpp>
#include <boost/mpl/int.hpp>
#include <boost/mpl/map.hpp>
#include <boost/mpl/vector.hpp>
#include <cstdint>
#include <iostream>
#include <type_traits>

struct A {
  int i = 42;
  virtual int foo() { return i; }
};
struct B : A {
  int foo() override { return i + 2; }
  virtual std::string bar() { return "B::BAR"; }
};
struct C : B {
  int foo() override { return i - 20; }
  std::string bar() { return "C::BAR"; }
};
using handle_t = std::uintptr_t;
enum class nature_t { A1, B1, C1 };

template <typename E>
constexpr auto to_integral(E e) -> typename std::underlying_type<E>::type {
  return static_cast<typename std::underlying_type<E>::type>(e);
}

auto to_enum(auto e) { return static_cast<nature_t>(e); }

template <class T> handle_t get(T &x) { return reinterpret_cast<handle_t>(&x); }
template <class type> nature_t type_to_value() {
  using lookup_table = boost::mpl::map<
      boost::mpl::pair<A *, boost::mpl::int_<to_integral(nature_t::A1)>>,
      boost::mpl::pair<B *, boost::mpl::int_<to_integral(nature_t::B1)>>,
      boost::mpl::pair<C *, boost::mpl::int_<to_integral(nature_t::C1)>>>;

  using lookup_result = typename boost::mpl::at<lookup_table, type>::type;
  return to_enum(lookup_result::value);
}

template <class T, class R, class F>
R black_magic(F f, handle_t h, nature_t n) {
  R result;
  boost::mpl::for_each<T>([n, h, f, &result](auto type_carrier) {
    using type = decltype(type_carrier);
    if (n == type_to_value<type>()) {
      std::cout << "will cast to " << to_integral(n) << "\n";
      result = f(reinterpret_cast<type>(h));
    }
  });
  return result;
}

auto ask_foo(handle_t h, nature_t n) {
  using accepted_types = boost::mpl::vector<A *, B *, C *>;
  return black_magic<accepted_types, int>([](A* p){return p->foo();}, h, n);
}

auto ask_bar(handle_t h, nature_t n) {
  using accepted_types = boost::mpl::vector<B *, C *>;
  return black_magic<accepted_types, std::string>([](B* p){return p->bar();}, h, n);
}


template <class T, class F> void test(F f) {
  T a;
  auto h = get(a);
  nature_t n = type_to_value<std::add_pointer_t<decltype(a)>>();
  auto r = f(h, n);
  std::cout << r << "\n";
}
int main() {
  test<A>(ask_foo);
  test<B>(ask_foo);
  test<C>(ask_foo);

  test<B>(ask_bar);
  test<C>(ask_bar);
  return 0;
}
+0 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte