progrium / darwinkit

Native Mac APIs for Go. Previously known as MacDriver
MIT License
5.04k stars 160 forks source link

Nib's InstantiateWithOwnerTopLevelObjects() not returning objects #233

Closed programmingkidx closed 11 months ago

programmingkidx commented 11 months ago

I am trying to port this Objective-c program to Go. What is does is loads a NIB file and uses it to display the interface. To run this program copy and paste it to a file called main.m, then run this command: clear ; clang -framework foundation -framework appkit main.m && ./a.out Download the included MainMenu.nib.zip file is included here, unzip it, and place the MenuMenu.nib file in the same folder as the a.out program. MainMenu.nib.zip

//Description: Finds and sets up a button on a window in a nib file
// Build directions: clang -framework foundation -framework appkit main.m
// Run directions: clear ; clang -framework foundation -framework appkit main.m && ./a.out

#import <Cocoa/Cocoa.h>

@interface MyClass : NSObject {
    NSTextField *textField;
}
- (void) doButton:(id) sender;
- (void) setTextField: (NSTextField *) newTextField;
@end

@implementation MyClass
- (void) doButton:(id) sender {
    NSLog(@"I'M ALIVE!!!");
    [textField setStringValue: @"OUCH!"];
}

- (void) setTextField: (NSTextField *) newTextField {
    textField = newTextField;
}

- (void) dealloc {
    [super dealloc];
    NSLog(@"Freeing MyClass object");
}
@end

int main(int argc, const char * argv[]) {
    NSData *data = [NSData dataWithContentsOfFile: @"MainMenu.nib"];
    if (data == nil) {
        NSLog(@"Failed to create data object");
        return -2;
    }
    NSLog(@"nsdata length: %lu", [data length]);

    NSNib *myNib = [[NSNib alloc] initWithNibData: data bundle: nil];
    if (myNib == nil) {
        NSLog(@"Error: failed to create nib object");
        return -1;
    }

    NSArray *myObjects = nil;
    NSLog(@"address of myObjects before: %p", myObjects);
    BOOL status;
    status = [myNib instantiateWithOwner: nil topLevelObjects:&myObjects];
    if (status == NO) {
        NSLog(@"Error: failed to instantiate nib file");
        return -6;
    }
    NSLog(@"address of myObjects after: %p", myObjects);
    NSLog(@"successful NIB instantiation: %d", status);
    NSLog(@"count: %lu", [myObjects count]);

    // print out all the objects found in the NIB file
    for(int i = 0; i < [myObjects count]; i++) {
        NSLog(@"Index %d: %@", i, myObjects[i]);
    }

    MyClass *myObj = [[MyClass alloc] init];
    BOOL foundWindow = NO;
    for(int i = 0; i < [myObjects count]; i++) {
        id theObject = [myObjects objectAtIndex: i];
        if ([theObject isKindOfClass: [NSWindow class]]) {
            [theObject orderFront: nil];
            foundWindow = YES;
            NSButton *aButton = [[theObject contentView] viewWithTag:1];
            if (aButton == nil) {
                NSLog(@"Failed to find button with tag 1");
                return -4;
            }
            [aButton setTarget: myObj];
            [aButton setAction: @selector(doButton:)];

            NSTextField *textField = [[theObject contentView] viewWithTag:2];
            if (textField == nil) {
                NSLog(@"Failed to find textfield with tag 2");
                return -5;
            }
            [myObj setTextField: textField];
            break;
        }
    }

    if (foundWindow == NO) {
        NSLog(@"Failed to find window in nib file");
        return -3;
    }
    return NSApplicationMain(argc, argv);
}

This is most of the Go version of the above program:

// File: main.go
// Date: 11/29/23
// Description: Uses a NIB file to display the interface
// Run directions: go run main.go

package main

import (
    "fmt"
    "os"
    "github.com/progrium/macdriver/macos/appkit"
    f "github.com/progrium/macdriver/macos/foundation"
    "github.com/progrium/macdriver/objc"
)

func main() {
    // Setup the application
    app := appkit.Application_SharedApplication()
    app.SetActivationPolicy(appkit.ApplicationActivationPolicyRegular)
    app.ActivateIgnoringOtherApps(true)

    // Get the NIB file's data
    godata, err := os.ReadFile("MainMenu.nib")
    if err != nil {
        fmt.Println("Failed to load nib file:", err)
        return
    }
    fmt.Println("godata length:", len(godata))

    // How the Nib would be obtained in Objective-C.
    // Can't use this method because InstantiateWithOwnerTopLevelObjects
    // does not accept (NS)Data object.
    nsdata := f.Data_DataWithContentsOfFile("MainMenu.nib")
    fmt.Println("nsdata length:", nsdata.Length())

    myNib := appkit.NewNibWithNibDataBundle(godata, nil)

    var myObjects []objc.IObject
    myObjects = nil                            // runs but no objects
    //myObjects = make([]objc.IObject, 0, 0)   // runs but no objects
    //myObjects = make([]objc.IObject, 0, 10)  // runs but no objects
    //myObjects = make([]objc.IObject, 10, 10) // causes panic

    fmt.Printf("address of myObjects before: %p\n", myObjects)
    status := myNib.InstantiateWithOwnerTopLevelObjects(nil, myObjects)
    if status == false {
        fmt.Println("Error: failed to instantiate nib file")
        return
    }
    fmt.Printf("address of myObjects after: %p\n", &myObjects)
    fmt.Println("successful NIB instantiation:", status)
    fmt.Println("count:", len(myObjects))

    // print out all the objects found in the NIB file
    for i := 0; i < len(myObjects); i++ {
        fmt.Printf("Index %d: %s", i, myObjects[i].Description())
    }
}

This Go program should do the same things as the Objective-c version. The issue appears to be that Nib's InstantiateWithOwnerTopLevelObjects() does not return the objects in the slice that is used as an argument to the method. I believe this could be a bug with DarwinKit.

Here is the output for the Objective-c program:

2023-12-06 11:03:09.211 a.out[1918:38326] nsdata length: 35871
2023-12-06 11:03:09.212 a.out[1918:38326] address of myObjects before: 0x0
2023-12-06 11:03:09.311 a.out[1918:38326] address of myObjects after: 0x6000007a4a50
2023-12-06 11:03:09.311 a.out[1918:38326] successful NIB instantiation: 1
2023-12-06 11:03:09.311 a.out[1918:38326] count: 5
2023-12-06 11:03:09.311 a.out[1918:38326] Index 0: <NSMenu: 0x600001c850c0>
    Title: Main Menu
    Supermenu: 0x0 (None), autoenable: YES
    Items:     (
        "<NSMenuItem: 0x6000022b8380 erase, submenu: 0x600001c84a40 (erase)>",
        "<NSMenuItem: 0x6000022b83f0 File, submenu: 0x600001c84f40 (File)>",
        "<NSMenuItem: 0x6000022b8460 Edit, submenu: 0x600001c84c40 (Edit)>",
        "<NSMenuItem: 0x6000022b84d0 Format, submenu: 0x600001c851c0 (Format)>",
        "<NSMenuItem: 0x6000022b8620 View, submenu: 0x600001c84840 (View)>",
        "<NSMenuItem: 0x6000022b8690 Window, submenu: 0x600001c85040 (Window)>",
        "<NSMenuItem: 0x6000022b8700 Help, submenu: 0x600001c84fc0 (Help)>"
    )
2023-12-06 11:03:09.311 a.out[1918:38326] Index 1: <NSObject: 0x600000bac010>
2023-12-06 11:03:09.311 a.out[1918:38326] Index 2: <NSFontManager: 0x600002aaefd0>
2023-12-06 11:03:09.311 a.out[1918:38326] Index 3: <NSWindow: 0x14b80f0f0>
2023-12-06 11:03:09.311 a.out[1918:38326] Index 4: <NSApplication: 0x14b82efe0>

Here is the output of the Go program:

godata length: 35871
nsdata length: 35871
address of myObjects before: 0x0
address of myObjects after: 0x1400000e0f0
successful NIB instantiation: true
count: 0

The Go program does correctly display the length of the NIB file. But it returns zero objects in the slice.

progrium commented 11 months ago

So far I haven't come across too many APIs that take an NSArray that will be written to, but I suspect that pattern is not working here because of the copying going on for converting from slices. If you were to use objc.Call to call InstantiateWithOwnerTopLevelObjects passing a constructed NSArray (maybe foundation.Array_Array()) it might work and I could look into how we might handle that in generated bindings.

programmingkidx commented 11 months ago

Your suggestion worked! I saw the correct output for the first time.

Here is the updated program with your suggestion implemented:

// File: main.go
// Date: 11/29/23
// Description: Uses a NIB file to display the interface
// Run directions: go run main.go

package main

import (
    "fmt"
    "os"
    "github.com/progrium/macdriver/macos/appkit"
    f "github.com/progrium/macdriver/macos/foundation"
    "github.com/progrium/macdriver/objc"
)

func main() {
    // Setup the application
    app := appkit.Application_SharedApplication()
    app.SetActivationPolicy(appkit.ApplicationActivationPolicyRegular)
    app.ActivateIgnoringOtherApps(true)

    // Get the NIB file's data
    godata, err := os.ReadFile("MainMenu.nib")
    if err != nil {
        fmt.Println("Failed to load nib file:", err)
        return
    }
    fmt.Println("godata length:", len(godata))

    // How the Nib would be obtained in Objective-C.
    // Can't use this method because InstantiateWithOwnerTopLevelObjects
    // does not accept (NS)Data object.
    nsdata := f.Data_DataWithContentsOfFile("MainMenu.nib")
    fmt.Println("nsdata length:", nsdata.Length())

    myNib := appkit.NewNibWithNibDataBundle(godata, nil)

    myObjects := f.Array_Array()

    fmt.Printf("address of myObjects before: %p\n", &myObjects)
    //status := myNib.InstantiateWithOwnerTopLevelObjects(nil, myObjects)
    status := objc.Call[bool](myNib, objc.Sel("instantiateWithOwner:topLevelObjects:"), nil, &myObjects)

    if status == false {
        fmt.Println("Error: failed to instantiate nib file")
        return
    }
    fmt.Printf("address of myObjects after: %p\n", &myObjects)
    fmt.Println("successful NIB instantiation:", status)
    fmt.Println("count:", myObjects.Count())

    // print out all the objects found in the NIB file
    var i uint
    for i = 0; i < myObjects.Count(); i++ {
        fmt.Printf("Index %d: %s\n", i, myObjects.ObjectAtIndex(i).Description())
    }
}

I do still like the idea of using a Go slice in place of a (NS)Array type. It more Go-friendly.

progrium commented 11 months ago

Yes that would be nice. I'm not even sure it's possible here. We convert/copy the Go slice to an NSArray and then because it's just passed by reference we have no way to know when to convert/copy it back to our slice, which we would have to keep track of and further complicate an already tricky memory management situation.

Glad it worked!

programmingkidx commented 11 months ago

Could we leave this issue open until the fix is in the repo?

progrium commented 11 months ago

What solution would make you want to close it?

programmingkidx commented 11 months ago

I think adding the rule to the bindings generator that prevents translating from an Objective-c type to a Go type when dealing with a pointer to a pointer in a parameter would fix the problem. One reason to use a pointer to a pointer is to return an object thru a parameter like an NSError.

Here are some of the methods that are probably affected by this issue:

progrium commented 11 months ago

That is a great suggestion that I think should work well but can we make it a separate issue?

programmingkidx commented 11 months ago

I think that is a good idea. This issue probably affects more than just NSNib's InstantiateWithOwnerTopLevelObjects().

programmingkidx commented 11 months ago

Created issue https://github.com/progrium/macdriver/issues/234 for the generator issue.