web-dev-qa-db-fra.com

Pourquoi les blocs nil / NULL provoquent-ils des erreurs de bus lors de leur exécution?

J'ai commencé à utiliser beaucoup de blocs et j'ai vite remarqué que des blocs nuls provoquaient des erreurs de bus:

typedef void (^SimpleBlock)(void);
SimpleBlock aBlock = nil;
aBlock(); // bus error

Cela semble aller à l'encontre du comportement habituel d'Objective-C qui ignore les messages aux objets nuls:

NSArray *foo = nil;
NSLog(@"%i", [foo count]); // runs fine

Par conséquent, je dois recourir au contrôle zéro habituel avant d'utiliser un bloc:

if (aBlock != nil)
    aBlock();

Ou utilisez des blocs factices:

aBlock = ^{};
aBlock(); // runs fine

Y a-t-il une autre option? Y a-t-il une raison pour laquelle aucun bloc ne peut être simplement un non?

73
zoul

Je voudrais expliquer cela un peu plus, avec une réponse plus complète. Considérons d'abord ce code:

#import <Foundation/Foundation.h>
int main(int argc, char *argv[]) {    
    void (^block)() = nil;
    block();
}

Si vous exécutez ceci, vous verrez un crash sur la ligne block() qui ressemble à ceci (lorsqu'il est exécuté sur une architecture 32 bits - c'est important):

EXC_BAD_ACCESS (code = 2, adresse = 0xc)

Alors, pourquoi ça? Eh bien, le 0xc Est l'élément le plus important. Le plantage signifie que le processeur a tenté de lire les informations à l'adresse mémoire 0xc. C'est presque certainement une chose tout à fait incorrecte à faire. Il est peu probable qu'il y ait quoi que ce soit là-bas. Mais pourquoi at-il essayé de lire cet emplacement mémoire? Eh bien, c'est dû à la façon dont un bloc est réellement construit sous le capot.

Lorsqu'un bloc est défini, le compilateur crée en fait une structure sur la pile, de cette forme:

struct Block_layout {
    void *isa;
    int flags;
    int reserved;
    void (*invoke)(void *, ...);
    struct Block_descriptor *descriptor;
    /* Imported variables. */
};

Le bloc est alors un pointeur sur cette structure. Le quatrième membre, invoke, de cette structure est le plus intéressant. Il s'agit d'un pointeur de fonction pointant vers le code où se trouve l'implémentation du bloc. Le processeur essaie donc de passer à ce code lorsqu'un bloc est appelé. Notez que si vous comptez le nombre d'octets dans la structure avant le membre invoke, vous constaterez qu'il y en a 12 en décimal ou C en hexadécimal.

Ainsi, lorsqu'un bloc est invoqué, le processeur prend l'adresse du bloc, ajoute 12 et essaie de charger la valeur contenue à cette adresse mémoire. Il essaie ensuite de passer à cette adresse. Mais si le bloc est nul, il essaiera de lire l'adresse 0xc. C'est une adresse duff, clairement, et nous obtenons donc le défaut de segmentation.

Maintenant, la raison pour laquelle il doit s'agir d'un plantage comme celui-ci plutôt que d'une défaillance silencieuse comme le fait un appel de message Objective-C est vraiment un choix de conception. Puisque le compilateur est en train de décider comment invoquer le bloc, il devrait injecter un code de vérification nul partout où un bloc est invoqué. Cela augmenterait la taille du code et entraînerait de mauvaises performances. Une autre option serait d'utiliser un trampoline qui vérifie le zéro. Cependant, cela entraînerait également une pénalité de performance. Les messages Objective-C passent déjà par un trampoline car ils doivent rechercher la méthode qui sera effectivement invoquée. Le runtime permet l'injection paresseuse de méthodes et le changement d'implémentations de méthodes, il passe donc déjà par un trampoline. La pénalité supplémentaire liée à la vérification de zéro n'est pas importante dans ce cas.

J'espère que cela aide un peu à expliquer la justification.

Pour plus d'informations, consultez mon blogposts .

139
mattjgalloway

La réponse de Matt Galloway est parfaite! Bonne lecture!

Je veux juste ajouter qu'il existe des moyens de faciliter la vie. Vous pouvez définir une macro comme celle-ci:

#define BLOCK_SAFE_RUN(block, ...) block ? block(__VA_ARGS__) : nil

Il peut prendre 0 à n arguments. Exemple d'utilisation

typedef void (^SimpleBlock)(void);
SimpleBlock simpleNilBlock = nil;
SimpleBlock simpleLogBlock = ^{ NSLog(@"working"); };
BLOCK_SAFE_RUN(simpleNilBlock);
BLOCK_SAFE_RUN(simpleLogBlock);

typedef void (^BlockWithArguments)(BOOL arg1, NSString *arg2);
BlockWithArguments argumentsNilBlock = nil;
BlockWithArguments argumentsLogBlock = ^(BOOL arg1, NSString *arg2) { NSLog(@"%@", arg2); };
BLOCK_SAFE_RUN(argumentsNilBlock, YES, @"ok");
BLOCK_SAFE_RUN(argumentsLogBlock, YES, @"ok");

Si vous voulez obtenir le valeur de retour du bloc et vous n'êtes pas sûr si le bloc existe ou non, alors vous feriez probablement mieux de simplement taper:

block ? block() : nil;

De cette façon, vous pouvez facilement définir la valeur de secours. Dans mon exemple, "zéro".

39
hfossli

Avertissement: je ne suis pas un expert des blocs.

Les blocs sont objets objective-c mais appeler un le bloc est pas un message , bien que vous puissiez toujours essayer [block retain]ing un bloc nil ou d'autres messages.

Espérons que cela (et les liens) aide.

8
Stephen Furlani

Ceci est ma solution la plus simple… Peut-être qu'il est possible d'écrire une fonction d'exécution universelle avec ces var-args c mais je ne sais pas comment écrire cela.

void run(void (^block)()) {
    if (block)block();
}

void runWith(void (^block)(id), id value) {
    if (block)block(value);
}
2
Renetik