Skip to content

Test that newer libobjcs let the Swift runtime set up a class's layout #21942

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 18, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import Resilient
import Foundation
import OneWordSuperclass

public class StaticClass: OneWordSuperclass {
@objc var first: Int32 = 0
var middle = GrowsToInt64()
@objc var last: Int = 0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are these (non-public) ivars @objc?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are the ones I'm testing the ivar offsets of. It doesn't really matter if they're public or not; Swift never exposes direct compile-time ivar access to Objective-C.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we not emit those records for non-@objc ivars too? I thought we had to so the objc runtime can slide them. In any case I'm fine with the @objc being here, just wondering if there was a specific reason

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we ever don't emit them for non-@objc ivars, I don't want the test to change. I think it's appropriate for something accessed through the ObjC runtime. (Besides, the non-@objc ivars aren't representable in Objective-C, and it feels weird to rely on them being there.)


@objc public static var offsetOfFirst: Int {
// IRGen lays out Swift classes that subclass Objective-C classes as if the
// only superclass was NSObject, so the starting (offset % alignment) isn't
// always 0. This means that on 32-bit platforms we'll have a gap *before*
// 'first' when we need 8-byte alignment, rather than after as you'd see in
// a struct (or base class).
return max(MemoryLayout<Int>.size, MemoryLayout<GrowsToInt64>.alignment) +
MemoryLayout<Int>.size
}

@objc public static var totalSize: Int {
return (2 * MemoryLayout<Int>.size) +
(2 * MemoryLayout<GrowsToInt64>.size) + // alignment
MemoryLayout<Int>.size
}
}

/// This class has the same layout as `StaticClass`, but will be accessed using
/// `NSClassFromString` instead of `+class`.
public class DynamicClass: OneWordSuperclass {
@objc var first: Int32 = 0
var middle = GrowsToInt64()
@objc var last: Int = 0

@objc public static var offsetOfFirst: Int {
// See above.
return max(MemoryLayout<Int>.size, MemoryLayout<GrowsToInt64>.alignment) +
MemoryLayout<Int>.size
}

@objc public static var totalSize: Int {
return (2 * MemoryLayout<Int>.size) +
(2 * MemoryLayout<GrowsToInt64>.size) + // alignment
MemoryLayout<Int>.size
}
}

public class PureSwiftBaseClass {
var word: Int64 = 0
}

public class PureSwiftClass: PureSwiftBaseClass {
@objc var first: Int32 = 0
var middle = GrowsToInt64()
@objc var last: Int = 0

@objc public static var offsetOfFirst: Int {
return (2 * MemoryLayout<Int>.size) + MemoryLayout<Int64>.size
}

@objc public static var totalSize: Int {
return (2 * MemoryLayout<Int>.size) + MemoryLayout<Int64>.size +
(2 * MemoryLayout<GrowsToInt64>.size) + // alignment
MemoryLayout<Int>.size
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@import Foundation;

@interface OneWordSuperclass : NSObject
@end
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#import "OneWordSuperclass.h"

@implementation OneWordSuperclass {
intptr_t unused;
}
@end
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
public struct GrowsToInt64 {
#if SMALL
var value: Int32
#elseif BIG
var value: Int64
#else
#error("Must define SMALL or BIG")
#endif

public init() { self.value = 0 }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module OneWordSuperclass {
header "OneWordSuperclass.h"
export *
}
78 changes: 78 additions & 0 deletions validation-test/Runtime/class-layout-from-objc.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Check that when Objective-C is first to touch a Swift class, it gives the
// Swift runtime a chance to update instance size and ivar offset metadata.

// RUN: %empty-directory(%t)
// RUN: %target-build-swift -emit-library -emit-module -o %t/libResilient.dylib %S/Inputs/class-layout-from-objc/Resilient.swift -Xlinker -install_name -Xlinker @executable_path/libResilient.dylib -Xfrontend -enable-resilience -DSMALL

// RUN: %target-clang -c %S/Inputs/class-layout-from-objc/OneWordSuperclass.m -fmodules -fobjc-arc -o %t/OneWordSuperclass.o
// RUN: %target-build-swift -emit-library -o %t/libClasses.dylib -emit-objc-header-path %t/Classes.h -I %t -I %S/Inputs/class-layout-from-objc/ %S/Inputs/class-layout-from-objc/Classes.swift %t/OneWordSuperclass.o -Xlinker -install_name -Xlinker @executable_path/libClasses.dylib -lResilient -L %t
// RUN: %target-clang %s -I %S/Inputs/class-layout-from-objc/ -I %t -fmodules -fobjc-arc -o %t/main -lResilient -lClasses -L %t
// RUN: %target-codesign %t/main %t/libResilient.dylib %t/libClasses.dylib
// RUN: %target-run %t/main OLD %t/libResilient.dylib %t/libClasses.dylib

// RUN: %target-build-swift -emit-library -emit-module -o %t/libResilient.dylib %S/Inputs/class-layout-from-objc/Resilient.swift -Xlinker -install_name -Xlinker @executable_path/libResilient.dylib -Xfrontend -enable-resilience -DBIG
// RUN: %target-codesign %t/libResilient.dylib
// RUN: %target-run %t/main NEW %t/libResilient.dylib %t/libClasses.dylib

// Try again when the class itself is also resilient.
// RUN: %target-build-swift -emit-library -o %t/libClasses.dylib -emit-objc-header-path %t/Classes.h -I %S/Inputs/class-layout-from-objc/ -I %t %S/Inputs/class-layout-from-objc/Classes.swift %t/OneWordSuperclass.o -Xlinker -install_name -Xlinker @executable_path/libClasses.dylib -lResilient -L %t
// RUN: %target-codesign %t/libClasses.dylib
// RUN: %target-run %t/main OLD %t/libResilient.dylib %t/libClasses.dylib

// RUN: %target-build-swift -emit-library -emit-module -o %t/libResilient.dylib %S/Inputs/class-layout-from-objc/Resilient.swift -Xlinker -install_name -Xlinker @executable_path/libResilient.dylib -Xfrontend -enable-resilience -DSMALL
// RUN: %target-codesign %t/libResilient.dylib
// RUN: %target-run %t/main NEW %t/libResilient.dylib %t/libClasses.dylib

// REQUIRES: executable_test
// REQUIRES: objc_interop

#import <objc/runtime.h>
#import <assert.h>
#import <dlfcn.h>
#import <stdbool.h>
#import <string.h>

#import "Classes.h"

void check(Class c) {
assert(c);

size_t expectedSize = [c totalSize];
size_t actualSize = class_getInstanceSize([c class]);
NSLog(@"%@: expected size %zd, actual size %zd", c, expectedSize, actualSize);
assert(expectedSize == actualSize);

size_t expectedOffsetOfFirst = [c offsetOfFirst];
size_t offsetOfFirst = ivar_getOffset(class_getInstanceVariable(c, "first"));
NSLog(@"expected offset of 'first' %zd, actual %zd",
expectedOffsetOfFirst, offsetOfFirst);
assert(offsetOfFirst == expectedOffsetOfFirst);

size_t offsetOfLast = ivar_getOffset(class_getInstanceVariable(c, "last"));
NSLog(@"offset of 'last' %zd", offsetOfLast);
assert(offsetOfLast == actualSize - sizeof(intptr_t));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it worth creating an instance and checking anything there as well, maybe calling a Swift method too?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't think of anything that wouldn't go through a Swift entry point except class_createInstance, and I think that's superfluous with checking the instance size.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the case I thought we could test is calling Swift code after Objective-C had already initialized the class metadata

Copy link
Contributor Author

@jrose-apple jrose-apple Jan 17, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, we call static methods. Think an instance method is interesting enough to also be worth doing?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably not.

}

int main(int argc, const char * const argv[]) {
assert(argc > 1);

if (!strcmp(argv[1], "OLD")) {
;
} else if (!strcmp(argv[1], "NEW")) {
// Only test the new behavior on a new enough libobjc.
if (!dlsym(RTLD_NEXT, "_objc_realizeClassFromSwift")) {
fprintf(stderr, "skipping evolution tests; OS too old\n");
return EXIT_SUCCESS;
}
} else {
fprintf(stderr, "usage: %s (OLD|NEW)\n", argv[0]);
return EXIT_FAILURE;
}

@autoreleasepool {
NSLog(@"%zd", class_getInstanceSize([OneWordSuperclass class]));
check([StaticClass class]);
check(objc_getClass("Classes.DynamicClass"));
check(objc_getClass("Classes.PureSwiftClass"));
}
}