Search code examples
objective-cobjective-c-blocks

Purpose of code blocks in this Objective-C method


I'm newish to objective C and and have a question about code from this example project from Apple showcasing some Metal features.

The code contains a few blocks sitting inside of the function scope. Inside each block, some resources are allocated, and the allocated resources are passed in to various methods.

What is the reasoning for doing this inside blocks within the function?

Also, I can see that the variables that are being declared inside each block are dropping off outside of block scope, but how does the memory managed for the resources those variables are pointing to? Is that managed by the autorelease pool?

- (void)loadMetal
{
    NSError* error = nil;
    NSLog(@"Selected Device: %@", _mtlDevice.name);
    _mtkView.colorPixelFormat = MTLPixelFormatBGRA8Unorm_sRGB;
    _mtkView.depthStencilPixelFormat = MTLPixelFormatDepth32Float;
    _commandQueue = [_mtlDevice newCommandQueue];

    id<MTLLibrary> defaultLibrary = [_mtlDevice newDefaultLibrary];

    // Create the render pipeline to shade the geometry.
    {
        id<MTLFunction> vertexFunc   = [defaultLibrary newFunctionWithName:@"vertexShader"];
        id<MTLFunction> fragmentFunc = [defaultLibrary newFunctionWithName:@"fragmentShader"];

        MTLRenderPipelineDescriptor* renderPipelineDesc = [MTLRenderPipelineDescriptor new];
        renderPipelineDesc.vertexFunction = vertexFunc;
        renderPipelineDesc.fragmentFunction = fragmentFunc;
        renderPipelineDesc.vertexDescriptor = nil;
        renderPipelineDesc.colorAttachments[0].pixelFormat = _mtkView.colorPixelFormat;
        renderPipelineDesc.depthAttachmentPixelFormat = _mtkView.depthStencilPixelFormat;
        // means "i don't need a depth buffer"
        renderPipelineDesc.stencilAttachmentPixelFormat = MTLPixelFormatInvalid;

        _renderPSO = [_mtlDevice newRenderPipelineStateWithDescriptor:renderPipelineDesc error:&error];
        NSAssert(_renderPSO, @"Failed to create forward plane with sparse texture render pipeline state");
    }

    // Create the default depth stencil state for the depth test.
    {
        MTLDepthStencilDescriptor* desc = [MTLDepthStencilDescriptor new];
        desc.depthWriteEnabled = YES;
        desc.depthCompareFunction = MTLCompareFunctionLess;
        _depthStencilState = [_mtlDevice newDepthStencilStateWithDescriptor:desc];
    }

    // Prefill the render pass descriptors with the clear, load, and store actions.
    {
        _renderPassDescriptor = [MTLRenderPassDescriptor new];
        _renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(0.1, 0.1, 0.1, 1.0);
        _renderPassDescriptor.colorAttachments[0].loadAction = MTLLoadActionClear;
        _renderPassDescriptor.colorAttachments[0].storeAction = MTLStoreActionStore;
        _renderPassDescriptor.depthAttachment.clearDepth = 1.0;
        _renderPassDescriptor.depthAttachment.loadAction = MTLLoadActionClear;
        _renderPassDescriptor.depthAttachment.storeAction = MTLStoreActionDontCare;
    }
}

Solution

  • What is the reasoning for doing this inside blocks within the function?

    First of all you should differentiate between Objective-C blocks, which are syntax sugar for functors in Objective-C (a first-class objects which can carry some task), which have the following syntax:

    ^{
        NSLog(@"Objective-C block");
    }
    

    And anonymous blocks, which are inherited from C and introduce a new scope:

    {
       printf("Anonymous block");
    }
    

    In the given code, it's an anonymous block, the main benefits of it is that it helps to decouple the code a little, i.e. the variables introduced inside the block are not visible outside of it. Another side effect, provided you have ARC enabled, is that the compiler ensures releasing all local non-retained objects at the end of the scope (but not necessarily deallocate).

    This seems somewhat redundant for the last piece of code, and omitting the anonymous block would not change how the function works here:

    // Prefill the render pass descriptors with the clear, load, and store actions.
    {
        _renderPassDescriptor = [MTLRenderPassDescriptor new];
        _renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(0.1, 0.1, 0.1, 1.0);
        _renderPassDescriptor.colorAttachments[0].loadAction = MTLLoadActionClear;
        _renderPassDescriptor.colorAttachments[0].storeAction = MTLStoreActionStore;
        _renderPassDescriptor.depthAttachment.clearDepth = 1.0;
        _renderPassDescriptor.depthAttachment.loadAction = MTLLoadActionClear;
        _renderPassDescriptor.depthAttachment.storeAction = MTLStoreActionDontCare;
    }
    

    ..but it still gives clear visual separation of different parts of the code for the reader.


    Also, I can see that the variables that are being declared inside each block are dropping off outside of block scope

    If you mean these parts:

    ...
    // Create the render pipeline to shade the geometry.
    {
        ...
        _renderPSO = [_mtlDevice newRenderPipelineStateWithDescriptor:renderPipelineDesc error:&error];
        NSAssert(_renderPSO, @"Failed to create forward plane with sparse texture render pipeline state");
    }
    
    // Create the default depth stencil state for the depth test.
    {
        ...
        _depthStencilState = [_mtlDevice newDepthStencilStateWithDescriptor:desc];
    }
    ...
    

    ..then the functions where the arguments are passed to in fact can retain the objects, but in Objective-C there is no controversy here: most objects in the language are allocated dynamically, so they can outlive the scope they were declared in. Usually ARC ensures for you that all variables referring to an object are valid as long as they are addressable:

    - (void)foo {
        NSObject *obj;
        {
            NSObject *localObj = [NSObject new];
            obj = localObj;
        }
        // The `obj` have the `localObj` retained here, so it keeps on living
        NSLog(@"%@", obj);
        // `obj` is released at the of the scope it was declared in;
    }
    
    

    Under the hood what ARC does can be read as follows:

    - (void)foo {
        NSObject *obj;
        {
            NSObject *localObj = [NSObject new];
            obj = [localObj retain];
            [localObj release];
        }
        // The `obj` have the `localObj` retained here, so it keeps on living
        NSLog(@"%@", obj);
        // `obj` is released at the of the scope it was declared in;
        [obj release];
    }