Commit 9836f1b2f9

Stephen Gutekanst <stephen@hexops.com>
2021-11-22 08:44:49
add support for compiling Objective-C++ code (#10096)
* add support for compiling Objective-C++ code Prior to this change, calling `step.addCSourceFiles` with Obj-C++ file extensions (`.mm`) would result in an error due to Zig not being aware of that extension. Clang supports an `-ObjC++` compilation mode flag, but it was only possible to use if you violated standards and renamed your `.mm` Obj-C++ files to `.m` (Obj-C) to workaround Zig being unaware of the extension. This change makes Zig aware of `.mm` files so they can be compiled, enabling compilation of projects such as [Google's Dawn WebGPU](https://dawn.googlesource.com/dawn/) using a `build.zig` file only. Helps hexops/mach#21 Signed-off-by: Stephen Gutekanst <stephen@hexops.com> * test/standalone: add ObjC++ compilation/linking test Based on the existing objc example, just tweaked for ObjC++. Signed-off-by: Stephen Gutekanst <stephen@hexops.com>
1 parent 722c6b9
Changed files (7)
src/Compilation.zig
@@ -3307,7 +3307,7 @@ pub fn addCCArgs(
     try argv.appendSlice(&[_][]const u8{ "-target", llvm_triple });
 
     switch (ext) {
-        .c, .cpp, .m, .h => {
+        .c, .cpp, .m, .mm, .h => {
             try argv.appendSlice(&[_][]const u8{
                 "-nostdinc",
                 "-fno-spell-checking",
@@ -3316,6 +3316,10 @@ pub fn addCCArgs(
                 try argv.append("-flto");
             }
 
+            if (ext == .mm) {
+                try argv.append("-ObjC++");
+            }
+
             // According to Rich Felker libc headers are supposed to go before C language headers.
             // However as noted by @dimenus, appending libc headers before c_headers breaks intrinsics
             // and other compiler specific items.
@@ -3599,6 +3603,7 @@ pub const FileExt = enum {
     cpp,
     h,
     m,
+    mm,
     ll,
     bc,
     assembly,
@@ -3610,7 +3615,7 @@ pub const FileExt = enum {
 
     pub fn clangSupportsDepFile(ext: FileExt) bool {
         return switch (ext) {
-            .c, .cpp, .h, .m => true,
+            .c, .cpp, .h, .m, .mm => true,
 
             .ll,
             .bc,
@@ -3648,6 +3653,10 @@ pub fn hasObjCExt(filename: []const u8) bool {
     return mem.endsWith(u8, filename, ".m");
 }
 
+pub fn hasObjCppExt(filename: []const u8) bool {
+    return mem.endsWith(u8, filename, ".mm");
+}
+
 pub fn hasAsmExt(filename: []const u8) bool {
     return mem.endsWith(u8, filename, ".s") or mem.endsWith(u8, filename, ".S");
 }
@@ -3686,6 +3695,8 @@ pub fn classifyFileExt(filename: []const u8) FileExt {
         return .cpp;
     } else if (hasObjCExt(filename)) {
         return .m;
+    } else if (hasObjCppExt(filename)) {
+        return .mm;
     } else if (mem.endsWith(u8, filename, ".ll")) {
         return .ll;
     } else if (mem.endsWith(u8, filename, ".bc")) {
@@ -3710,6 +3721,7 @@ pub fn classifyFileExt(filename: []const u8) FileExt {
 test "classifyFileExt" {
     try std.testing.expectEqual(FileExt.cpp, classifyFileExt("foo.cc"));
     try std.testing.expectEqual(FileExt.m, classifyFileExt("foo.m"));
+    try std.testing.expectEqual(FileExt.mm, classifyFileExt("foo.mm"));
     try std.testing.expectEqual(FileExt.unknown, classifyFileExt("foo.nim"));
     try std.testing.expectEqual(FileExt.shared_library, classifyFileExt("foo.so"));
     try std.testing.expectEqual(FileExt.shared_library, classifyFileExt("foo.so.1"));
src/main.zig
@@ -296,6 +296,7 @@ const usage_build_generic =
     \\                      .c    C source code (requires LLVM extensions)
     \\        .cxx .cc .C .cpp    C++ source code (requires LLVM extensions)
     \\                      .m    Objective-C source code (requires LLVM extensions)
+    \\                     .mm    Objective-C++ source code (requires LLVM extensions)
     \\                     .bc    LLVM IR Module (requires LLVM extensions)
     \\
     \\General Options:
@@ -1190,7 +1191,7 @@ fn buildOutputType(
                     .object, .static_library, .shared_library => {
                         try link_objects.append(arg);
                     },
-                    .assembly, .c, .cpp, .h, .ll, .bc, .m => {
+                    .assembly, .c, .cpp, .h, .ll, .bc, .m, .mm => {
                         try c_source_files.append(.{
                             .src_path = arg,
                             .extra_flags = try arena.dupe([]const u8, extra_cflags.items),
@@ -1256,7 +1257,7 @@ fn buildOutputType(
                     .positional => {
                         const file_ext = Compilation.classifyFileExt(mem.spanZ(it.only_arg));
                         switch (file_ext) {
-                            .assembly, .c, .cpp, .ll, .bc, .h, .m => try c_source_files.append(.{ .src_path = it.only_arg }),
+                            .assembly, .c, .cpp, .ll, .bc, .h, .m, .mm => try c_source_files.append(.{ .src_path = it.only_arg }),
                             .unknown, .shared_library, .object, .static_library => {
                                 try link_objects.append(it.only_arg);
                             },
test/standalone/objcpp/build.zig
@@ -0,0 +1,36 @@
+const std = @import("std");
+const Builder = std.build.Builder;
+const CrossTarget = std.zig.CrossTarget;
+
+fn isRunnableTarget(t: CrossTarget) bool {
+    // TODO I think we might be able to run this on Linux via Darling.
+    // Add a check for that here, and return true if Darling is available.
+    if (t.isNative() and t.getOsTag() == .macos)
+        return true
+    else
+        return false;
+}
+
+pub fn build(b: *Builder) void {
+    const mode = b.standardReleaseOptions();
+    const target = b.standardTargetOptions(.{});
+
+    const test_step = b.step("test", "Test the program");
+
+    const exe = b.addExecutable("test", null);
+    b.default_step.dependOn(&exe.step);
+    exe.addIncludeDir(".");
+    exe.addCSourceFile("Foo.mm", &[0][]const u8{});
+    exe.addCSourceFile("test.mm", &[0][]const u8{});
+    exe.setBuildMode(mode);
+    exe.setTarget(target);
+    exe.linkLibCpp();
+    // TODO when we figure out how to ship framework stubs for cross-compilation,
+    // populate paths to the sysroot here.
+    exe.linkFramework("Foundation");
+
+    if (isRunnableTarget(target)) {
+        const run_cmd = exe.run();
+        test_step.dependOn(&run_cmd.step);
+    }
+}
test/standalone/objcpp/Foo.h
@@ -0,0 +1,7 @@
+#import <Foundation/Foundation.h>
+
+@interface Foo : NSObject
+
+- (NSString *)name;
+
+@end
test/standalone/objcpp/Foo.mm
@@ -0,0 +1,11 @@
+#import "Foo.h"
+
+@implementation Foo
+
+- (NSString *)name
+{
+      NSString *str = [[NSString alloc] initWithFormat:@"Zig"];
+      return str;
+}
+
+@end
test/standalone/objcpp/test.mm
@@ -0,0 +1,14 @@
+#import "Foo.h"
+#import <assert.h>
+#include <iostream>
+
+int main(int argc, char *argv[])
+{
+  @autoreleasepool {
+      Foo *foo = [[Foo alloc] init];
+      NSString *result = [foo name];
+      std::cout << "Hello from C++ and " << [result UTF8String];
+      assert([result isEqualToString:@"Zig"]);
+      return 0;
+  }
+}
test/standalone.zig
@@ -69,6 +69,11 @@ pub fn addCases(cases: *tests.StandaloneContext) void {
         .build_modes = true,
         .requires_macos_sdk = true,
     });
+    // Try to build and run an Objective-C++ executable.
+    cases.addBuildFile("test/standalone/objcpp/build.zig", .{
+        .build_modes = true,
+        .requires_macos_sdk = true,
+    });
 
     // Ensure the development tools are buildable.
     cases.add("tools/gen_spirv_spec.zig");