Skip to content

Commit

Permalink
Add /:target option and targets subcommand
Browse files Browse the repository at this point in the history
  • Loading branch information
squeek502 committed Nov 3, 2024
1 parent 66dbf7e commit 6d1ea48
Show file tree
Hide file tree
Showing 4 changed files with 213 additions and 18 deletions.
51 changes: 49 additions & 2 deletions src/cli.zig
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,18 @@ const lang = @import("lang.zig");
const res = @import("res.zig");
const Allocator = std.mem.Allocator;
const lex = @import("lex.zig");
const cvtres = @import("cvtres.zig");

/// This is what /SL 100 will set the maximum string literal length to
pub const max_string_literal_length_100_percent = 8192;

pub const usage_string_after_command_name =
pub const usage_string1_after_command_name =
\\ [options] [--] <INPUT> [<OUTPUT>]
\\
;
pub const usage_string2_after_command_name =
\\ <subcommand> [options]
\\
\\The sequence -- can be used to signify when to stop parsing options.
\\This is necessary when the input path begins with a forward slash.
\\
Expand Down Expand Up @@ -67,15 +72,29 @@ pub const usage_string_after_command_name =
\\ res (default if output format cannot be inferred)
\\ coff COFF object file (extension: .obj or .o)
\\ rcpp Preprocessed .rc file, implies /p
\\ /:target <arch> Set the target machine for COFF object files.
\\ Can be specified either as PE/COFF machine constant
\\ name (X64, ARM64, etc) or Zig/LLVM CPU name (x86_64,
\\ aarch64, etc). The default is X64 (aka x86_64).
\\ Also accepts a full Zig/LLVM triple, but everything
\\ except the architecture is ignored.
\\ Use the targets subcommand to see the list of all
\\ supported target architectures.
\\
\\Note: For compatibility reasons, all custom options start with :
\\
\\Subcommands:
\\ targets Output a list of all supported /:target values.
\\
;

pub fn writeUsage(writer: anytype, command_name: []const u8) !void {
try writer.writeAll("Usage: ");
try writer.writeAll(command_name);
try writer.writeAll(usage_string_after_command_name);
try writer.writeAll(usage_string1_after_command_name);
try writer.writeAll(" ");
try writer.writeAll(command_name);
try writer.writeAll(usage_string2_after_command_name);
}

pub const Diagnostics = struct {
Expand Down Expand Up @@ -159,6 +178,7 @@ pub const Options = struct {
depfile_fmt: DepfileFormat = .json,
input_format: InputFormat = .rc,
output_format: OutputFormat = .res,
target: std.coff.MachineType = .X64,

pub const AutoIncludes = enum { any, msvc, gnu, none };
pub const DepfileFormat = enum { json };
Expand Down Expand Up @@ -273,6 +293,9 @@ pub const Options = struct {
pub fn dumpVerbose(self: *const Options, writer: anytype) !void {
try writer.print("Input filename: {s} (format={s})\n", .{ self.input_filename, @tagName(self.input_format) });
try writer.print("Output filename: {s} (format={s})\n", .{ self.output_filename, @tagName(self.output_format) });
if (self.output_format == .coff) {
try writer.print(" Target machine type for COFF: {s}\n", .{@tagName(self.target)});
}
if (self.extra_include_paths.items.len > 0) {
try writer.writeAll(" Extra include paths:\n");
for (self.extra_include_paths.items) |extra_include_path| {
Expand Down Expand Up @@ -567,6 +590,30 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn
depfile_context = .{ .index = arg_i, .option_len = ":depfile".len, .arg = arg, .value = value };
arg_i += value.index_increment;
continue :next_arg;
} else if (std.ascii.startsWithIgnoreCase(arg_name, ":target")) {
const value = arg.value(":target".len, arg_i, args) catch {
var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
var msg_writer = err_details.msg.writer(allocator);
try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(":target".len) });
try diagnostics.append(err_details);
arg_i += 1;
break :next_arg;
};
// Take the substring up to the first dash so that a full target triple
// can be used, e.g. x86_64-windows-gnu becomes x86_64
var target_it = std.mem.splitScalar(u8, value.slice, '-');
const arch_str = target_it.first();
const arch = cvtres.supported_targets.Arch.fromStringIgnoreCase(arch_str) orelse {
var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
var msg_writer = err_details.msg.writer(allocator);
try msg_writer.print("invalid or unsupported target architecture: {s} ", .{arch_str});
try diagnostics.append(err_details);
arg_i += value.index_increment;
continue :next_arg;
};
options.target = arch.toCoffMachineType();
arg_i += value.index_increment;
continue :next_arg;
} else if (std.ascii.startsWithIgnoreCase(arg_name, "nologo")) {
// No-op, we don't display any 'logo' to suppress
arg.name_offset += "nologo".len;
Expand Down
150 changes: 136 additions & 14 deletions src/cvtres.zig
Original file line number Diff line number Diff line change
Expand Up @@ -156,19 +156,6 @@ pub fn parseNameOrOrdinal(allocator: Allocator, reader: anytype) !NameOrOrdinal
return .{ .name = try name_buf.toOwnedSliceSentinel(allocator, 0) };
}

// https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#type-indicators
pub fn rvaRelocationTypeIndicator(target: std.coff.MachineType) u16 {
return switch (target) {
.X64 => 0x3, // IMAGE_REL_AMD64_ADDR32NB
.I386 => 0x7, // IMAGE_REL_I386_DIR32NB
.ARM, .ARMNT => 0x2, // IMAGE_REL_ARM_ADDR32NB
.ARM64, .ARM64EC, .ARM64X => 0x2, // IMAGE_REL_ARM64_ADDR32NB
.IA64 => 0x10, // IMAGE_REL_IA64_DIR32NB
.EBC => 0x1, // This is what cvtres.exe writes for this target, unsure where it comes from
else => 0,
};
}

pub const CoffOptions = struct {
target: std.coff.MachineType = .X64,
/// If true, zeroes will be written to all timestamp fields
Expand Down Expand Up @@ -739,7 +726,7 @@ const ResourceTree = struct {
try writeRelocation(w, std.coff.Relocation{
.virtual_address = relocation.relocation_address,
.symbol_table_index = relocation.symbol_index,
.type = rvaRelocationTypeIndicator(options.target),
.type = supported_targets.rvaRelocationTypeIndicator(options.target).?,
});
}

Expand Down Expand Up @@ -835,6 +822,141 @@ const Relocations = struct {
}
};

pub const supported_targets = struct {
/// Enum containing a mixture of names that come from:
/// - Machine Types constants in the PE format spec:
/// https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#machine-types
/// - cvtres.exe /machine options
/// - Zig/LLVM arch names
/// All field names are lowercase regardless of their casing used in the above origins.
pub const Arch = enum {
// cvtres.exe /machine names
x64,
x86,
/// Note: Following cvtres.exe's lead, this corresponds to ARMNT, not ARM
arm,
arm64,
arm64ec,
arm64x,
ia64,
ebc,

// PE/COFF MACHINE constant names not covered above
amd64,
i386,
armnt,

// Zig/LLVM names not already covered above
x86_64,
aarch64,

pub fn toCoffMachineType(arch: Arch) std.coff.MachineType {
return switch (arch) {
.x64, .amd64, .x86_64 => .X64,
.x86, .i386 => .I386,
.arm, .armnt => .ARMNT,
.arm64, .aarch64 => .ARM64,
.arm64ec => .ARM64EC,
.arm64x => .ARM64X,
.ia64 => .IA64,
.ebc => .EBC,
};
}

pub fn description(arch: Arch) []const u8 {
return switch (arch) {
.x64, .amd64, .x86_64 => "64-bit X86",
.x86, .i386 => "32-bit X86",
.arm, .armnt => "ARM Thumb-2 little endian",
.arm64, .aarch64 => "ARM64/AArch64 little endian",
.arm64ec => "ARM64 \"Emulation Compatible\"",
.arm64x => "ARM64 and ARM64EC together",
.ia64 => "64-bit Intel Itanium",
.ebc => "EFI Byte Code",
};
}

pub const ordered_for_display: []const Arch = &.{
.x64,
.x86_64,
.amd64,
.x86,
.i386,
.arm64,
.aarch64,
.arm,
.armnt,
.arm64ec,
.arm64x,
.ia64,
.ebc,
};
comptime {
for (@typeInfo(Arch).@"enum".fields) |enum_field| {
_ = std.mem.indexOfScalar(Arch, ordered_for_display, @enumFromInt(enum_field.value)) orelse {
@compileError(std.fmt.comptimePrint("'{s}' missing from ordered_for_display", .{enum_field.name}));
};
}
}

pub const longest_name = blk: {
var len = 0;
for (@typeInfo(Arch).@"enum".fields) |field| {
if (field.name.len > len) len = field.name.len;
}
break :blk len;
};

pub fn fromStringIgnoreCase(str: []const u8) ?Arch {
if (str.len > longest_name) return null;
var lower_buf: [longest_name]u8 = undefined;
const lower = std.ascii.lowerString(&lower_buf, str);
return std.meta.stringToEnum(Arch, lower);
}

test fromStringIgnoreCase {
try std.testing.expectEqual(.x64, Arch.fromStringIgnoreCase("x64").?);
try std.testing.expectEqual(.x64, Arch.fromStringIgnoreCase("X64").?);
try std.testing.expectEqual(.aarch64, Arch.fromStringIgnoreCase("Aarch64").?);
try std.testing.expectEqual(null, Arch.fromStringIgnoreCase("armzzz"));
try std.testing.expectEqual(null, Arch.fromStringIgnoreCase("long string that is longer than any field"));
}
};

// https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#type-indicators
pub fn rvaRelocationTypeIndicator(target: std.coff.MachineType) ?u16 {
return switch (target) {
.X64 => 0x3, // IMAGE_REL_AMD64_ADDR32NB
.I386 => 0x7, // IMAGE_REL_I386_DIR32NB
.ARMNT => 0x2, // IMAGE_REL_ARM_ADDR32NB
.ARM64, .ARM64EC, .ARM64X => 0x2, // IMAGE_REL_ARM64_ADDR32NB
.IA64 => 0x10, // IMAGE_REL_IA64_DIR32NB
.EBC => 0x1, // This is what cvtres.exe writes for this target, unsure where it comes from
else => null,
};
}

pub fn isSupported(target: std.coff.MachineType) bool {
return rvaRelocationTypeIndicator(target) != null;
}

comptime {
// Enforce two things:
// 1. Arch enum field names are all lowercase (necessary for how fromStringIgnoreCase is implemented)
// 2. All enum fields in Arch have an associated RVA relocation type when converted to a coff.MachineType
for (@typeInfo(Arch).@"enum".fields) |enum_field| {
const all_lower = all_lower: for (enum_field.name) |c| {
if (std.ascii.isUpper(c)) break :all_lower false;
} else break :all_lower true;
if (!all_lower) @compileError(std.fmt.comptimePrint("Arch field is not all lowercase: {s}", .{enum_field.name}));
const coff_machine = @field(Arch, enum_field.name).toCoffMachineType();
_ = rvaRelocationTypeIndicator(coff_machine) orelse {
@compileError(std.fmt.comptimePrint("No RVA relocation for Arch: {s}", .{enum_field.name}));
};
}
}
};

test writeCoff {
var buf = std.ArrayList(u8).init(std.testing.allocator);
defer buf.deinit();
Expand Down
9 changes: 7 additions & 2 deletions src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const auto_includes = @import("auto_includes.zig");
const hasDisjointCodePage = @import("disjoint_code_page.zig").hasDisjointCodePage;
const cvtres = @import("cvtres.zig");
const aro = @import("aro");
const subcommands = @import("subcommands.zig");

pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{ .stack_trace_frames = 8 }){};
Expand All @@ -36,6 +37,11 @@ pub fn main() !void {
defer std.process.argsFree(allocator, all_args);
const args = all_args[1..]; // skip past the executable name

if (args.len > 0 and std.mem.eql(u8, args[0], "targets")) {
try subcommands.targets.run();
return;
}

var cli_diagnostics = cli.Diagnostics.init(allocator);
defer cli_diagnostics.deinit();
var options = cli.parse(allocator, args, &cli_diagnostics) catch |err| switch (err) {
Expand Down Expand Up @@ -347,8 +353,7 @@ pub fn main() !void {
var coff_output_buffered_stream = std.io.bufferedWriter(coff_output_file.writer());

cvtres.writeCoff(allocator, coff_output_buffered_stream.writer(), resources, .{
// TODO: Make this configurable
.target = .X64,
.target = options.target,
}) catch |err| {
// TODO: Better errors
try renderErrorMessage(stderr.writer(), stderr_config, .err, "unable to write coff output file '{s}': {s}", .{ coff_output_filename, @errorName(err) });
Expand Down
21 changes: 21 additions & 0 deletions src/subcommands.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const std = @import("std");
const supported_targets = @import("cvtres.zig").supported_targets;

pub const targets = struct {
pub fn run() !void {
var buffered_stdout = std.io.bufferedWriter(std.io.getStdOut().writer());
const w = buffered_stdout.writer();

for (supported_targets.Arch.ordered_for_display) |arch| {
try w.print("{s: <" ++ std.fmt.comptimePrint("{}", .{supported_targets.Arch.longest_name + 2}) ++ "} {s}\n", .{ @tagName(arch), arch.description() });
}

try w.writeAll(
\\
\\Note: 'arm' is an alias for 'armnt' to match how the /MACHINE option works in cvtres.exe.
\\ This means that there is currently no way to target 32-bit ARM without Thumb-2.
);

try buffered_stdout.flush();
}
};

0 comments on commit 6d1ea48

Please sign in to comment.