-
Notifications
You must be signed in to change notification settings - Fork 6
Add XGCompiler class #13
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,11 @@ class GoDShellManager { | |
| enum Commands { | ||
| case wit | ||
| case wimgt | ||
| case gcc | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The indentation is a bit funny here but no big deal
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. whoops |
||
| case ld | ||
| case nm | ||
| case objdump | ||
| case objcopy | ||
| case gcitool | ||
| case gcitoolReplace | ||
| case pbrSaveTool | ||
|
|
@@ -21,6 +26,11 @@ class GoDShellManager { | |
| switch self { | ||
| case .wit: return .wit | ||
| case .wimgt: return .wimgt | ||
| case .gcc: return .gcc | ||
| case .ld: return .ld | ||
| case .nm: return .nm | ||
| case .objdump: return .objdump | ||
| case .objcopy: return .objcopy | ||
| case .gcitool: return .tool("gcitool") | ||
| case .gcitoolReplace: return .tool("gcitool_replace") | ||
| case .pbrSaveTool: return .tool("pbrsavetool") | ||
|
|
@@ -79,49 +89,4 @@ class GoDShellManager { | |
| return output | ||
| } | ||
|
|
||
| @discardableResult | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did you mean to remove this?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nope, my bad! |
||
| static func runAsync(_ command: Commands, args: String? = nil, inputRedirectFile: XGFiles? = nil, outputRedirectFile: XGFiles? = nil, errorRedirectFile: XGFiles? = nil) -> GoDProcess? { | ||
| guard command.file.exists else { | ||
| printg("command, \(command.file.fileName), doesn't exist\n\(command.file.path) not found") | ||
| return nil | ||
| } | ||
|
|
||
| let process = Process() | ||
| process.executableURL = URL(fileURLWithPath: command.file.path) | ||
| if let args = args { | ||
| let escaped = args.replacingOccurrences(of: "\\ ", with: "<!SPACE>") | ||
| process.arguments = escaped.split(separator: " ").compactMap(String.init).map({ (arg) -> String in | ||
| return arg.replacingOccurrences(of: "<!SPACE>", with: " ") | ||
| }) | ||
| } | ||
|
|
||
| if let inputFile = inputRedirectFile, inputFile.exists { | ||
| let fileHandle = FileHandle(forReadingAtPath: inputFile.path) | ||
| process.standardInput = fileHandle | ||
| } | ||
|
|
||
| if let outputFile = outputRedirectFile { | ||
| let fileHandle = FileHandle(forWritingAtPath: outputFile.path) | ||
| process.standardOutput = fileHandle | ||
| } else { | ||
| process.standardOutput = nil | ||
| } | ||
|
|
||
| if let errorFile = errorRedirectFile { | ||
| let fileHandle = FileHandle(forWritingAtPath: errorFile.path) | ||
| process.standardError = fileHandle | ||
| } else { | ||
| process.standardError = nil | ||
| } | ||
|
|
||
| do { | ||
| try process.run() | ||
| return GoDProcess(process: process) | ||
| } catch let error { | ||
| printg("Shell error:", error) | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,272 @@ | ||
| import Foundation | ||
|
|
||
| enum XGCompilerTarget { | ||
| case ogc | ||
| case bare | ||
| } | ||
|
|
||
| class XGCompiler: NSObject { | ||
| class func compileBinary(startOffset: Int, type: XGCompilerTarget = .bare, sources: [String], linkerScripts: [String], directoryPath: String) -> (instructions: [UInt32], exports: [String:UInt32], patches: [UInt32:UInt32]) { | ||
| var offset = startOffset | ||
| if offset < 0x80000000 { | ||
| offset += 0x80000000 | ||
| } | ||
|
|
||
| let directory = URL(fileURLWithPath: directoryPath) | ||
|
|
||
| let compiledFileURL = directory.appendingPathComponent("code.elf") | ||
| try? FileManager.default.removeItem(at: compiledFileURL) | ||
|
|
||
| var args = sources.map{ directory.appendingPathComponent($0).path.escapedPath }.joined(separator: " ") | ||
| args += " -o \(compiledFileURL.path.escapedPath)" | ||
| args += " -I\(directoryPath.escapedPath)" | ||
| args += " -O1 -mcpu=750 -meabi -mhard-float" | ||
| args += " -mno-sdata -Wno-builtin-declaration-mismatch" | ||
| args += " -ffast-math -flto -fwhole-program -fdata-sections -fkeep-static-functions" | ||
| args += " -fno-tree-loop-distribute-patterns -fno-zero-initialized-in-bss" | ||
| args += " -Wl,-Map=\(directory.appendingPathComponent("code.map").path.escapedPath)" | ||
| args += linkerScripts.map { | ||
| var path = directory.appendingPathComponent($0).path.escapedPath | ||
| if $0.prefix(1) == "/" { | ||
| path = $0.escapedPath | ||
| } | ||
| return " -T \(path)" | ||
| }.joined(separator: " ") | ||
| switch type { | ||
| case .ogc: | ||
| args += " -I/opt/devkitpro/libogc/include -DGEKKO -mogc" | ||
| args += " -L/opt/devkitpro/libogc/lib/cube -ldb -lbba -lmad -lasnd -logc -lm" | ||
| args += " -Wl,--gc-sections -Wl,--section-start,.init=\(offset.hexString())" | ||
| case .bare: | ||
| args += " -nolibc -nostdlib -nodefaultlibs" | ||
| args += " -Wl,--gc-sections -Wl,--section-start,.text=\(offset.hexString())" | ||
| } | ||
|
|
||
| // print(args) | ||
| GoDShellManager.run(.gcc, args: args, printOutput: false) | ||
|
|
||
| var out = GoDShellManager.run(.objcopy, args: "--remove-section .comment \(compiledFileURL.path.escapedPath)", printOutput: false) | ||
| print(out!.dropLast()) | ||
|
|
||
| out = GoDShellManager.run(.nm, args: "-a \(compiledFileURL.path.escapedPath)", printOutput: false) | ||
| let exports = XGCompiler.getExports(out!) | ||
| let imports = XGCompiler.getImports(out!) | ||
|
|
||
| let patches = XGCompiler.getPatches(sources.map{ directory.appendingPathComponent($0).path }, imports: imports, exports: exports) | ||
|
|
||
| if settings.verbose { | ||
| out = GoDShellManager.run(.objdump, args: "-D \(compiledFileURL.path.escapedPath)", printOutput: false) | ||
| printg("generated asm: \(out!.dropLast())") | ||
| } | ||
|
|
||
| let codeBinaryFileURL = directory.appendingPathComponent("code.bin") | ||
| GoDShellManager.run(.objcopy, args: "-O binary \(compiledFileURL.path.escapedPath) \(codeBinaryFileURL.path.escapedPath)", printOutput: false) | ||
|
|
||
| guard var codeBinaryData = try? Data(contentsOf: codeBinaryFileURL).rawBytes else { | ||
| fatalError("Could not read output compilation output") | ||
| } | ||
|
|
||
| // pad to the nearest word | ||
| let words = codeBinaryData.count / 4 | ||
| let rem = codeBinaryData.count - (words * 4) | ||
| let padding = 4 - rem | ||
| codeBinaryData.append(contentsOf: [UInt8](repeating: 0, count: padding)) | ||
|
|
||
| let instructions = codeBinaryData.withUnsafeBytes { | ||
| Array($0.bindMemory(to: UInt32.self)).map(UInt32.init(bigEndian:)) | ||
| } | ||
|
|
||
| return (instructions, exports, patches) | ||
| } | ||
|
|
||
| class func compileCode(startOffset: Int, code: String, include: String = "", externSymbols: [String:UInt32] = [:]) -> (instructions: [UInt32], exports: [String:UInt32], patches: [UInt32:UInt32]) { | ||
| let funcName = "__func" | ||
|
|
||
| var offset = startOffset | ||
| if offset < 0x80000000 { | ||
| offset += 0x80000000 | ||
| } | ||
|
|
||
| let tmpDir = try? XGTemporaryDirectory() | ||
| // tmpDir!.keepDirectory = true | ||
| let tmpDirPath = tmpDir!.directoryURL.path | ||
| let folder = XGFolders.path(tmpDirPath) | ||
|
|
||
| let includeFile = XGFiles.nameAndFolder("code.h", folder) | ||
| XGUtility.saveString(XGCompilerConstants.dolphinIncludes+include, toFile: includeFile) | ||
|
|
||
| let codeFile = XGFiles.nameAndFolder("code.c", folder) | ||
| XGUtility.saveString(""" | ||
| #include "code.h" | ||
| void \(funcName)() { | ||
| \(code) | ||
| } | ||
| """, toFile: codeFile) | ||
|
|
||
| let linkFile = XGFiles.nameAndFolder("ngc.ld", folder) | ||
| XGUtility.saveString(generateLinkerScript(offset: offset, name: funcName), toFile: linkFile) | ||
|
|
||
| let mapFile = XGFiles.nameAndFolder("map.ld", folder) | ||
| XGUtility.saveString(generateMapScript(), toFile: mapFile) | ||
|
|
||
| let externFile = XGFiles.nameAndFolder("extern.ld", folder) | ||
| XGUtility.saveString(generateExternScript(externSymbols), toFile: externFile) | ||
|
|
||
| return XGCompiler.compileBinary(startOffset: startOffset, type: .bare, sources: ["code.c"], linkerScripts: ["ngc.ld", "map.ld", "external.ld"], directoryPath: tmpDirPath) | ||
| } | ||
|
|
||
| class func getPatches(_ sources: [String], imports: [String:UInt32], exports: [String:UInt32]) -> [UInt32:UInt32] { | ||
| var patches : [UInt32:UInt32] = [:] | ||
|
|
||
| let pattern = "^PATCH_CALL\\((.*), (.*)\\)" | ||
| let regex = try! NSRegularExpression(pattern: pattern) | ||
|
|
||
| for source in sources { | ||
| let contents = try! String(contentsOf: URL(fileURLWithPath: source)) | ||
| let lines = contents.components(separatedBy: CharacterSet.newlines) | ||
|
|
||
| for line in lines { | ||
| let lineRange = NSRange(location: 0, length: line.utf16.count) | ||
| guard let match = regex.firstMatch(in: line, options: [], range: lineRange) else { continue } | ||
|
|
||
| let locationRange = Range(match.range(at: 1), in: line)! | ||
| let location = String(line[locationRange]) | ||
|
|
||
| var address : UInt32 | ||
| if let val = imports[location] { | ||
| address = val | ||
| } else { | ||
| address = UInt32(location.dropFirst(2), radix: 16)! | ||
| } | ||
|
|
||
| let symbolRange = Range(match.range(at: 2), in: line)! | ||
| let symbol = String(line[symbolRange]) | ||
|
|
||
| let code = exports[symbol]! | ||
|
|
||
| patches[address] = code | ||
| } | ||
|
|
||
| } | ||
|
|
||
| return patches | ||
| } | ||
|
|
||
| class func applyPatches(_ patches: [UInt32:UInt32]) { | ||
| for (absCallLocation, absCallTarget) in patches { | ||
| let callLocation = Int(absCallLocation) - 0x80000000 | ||
| let callTarget = Int(absCallTarget) - 0x80000000 | ||
| print("Patching function call at \(absCallLocation.hexString())") | ||
| XGAssembly.replaceRamASM(RAMOffset: callLocation, newASM: [.bl(callTarget)]) | ||
| } | ||
| } | ||
|
|
||
| class func getExports(_ text: String) -> [String:UInt32] { | ||
| return XGCompiler.parseNameMangling(text, include: ["T", "t", "d"]) | ||
| } | ||
|
|
||
| class func getImports(_ text: String) -> [String:UInt32] { | ||
| return XGCompiler.parseNameMangling(text, include: ["A"]) | ||
| } | ||
|
|
||
| class func parseNameMangling(_ text: String, include: [Character]) -> [String:UInt32] { | ||
| let lines = text.components(separatedBy: CharacterSet.newlines) | ||
|
|
||
| var symbols: [String:UInt32] = [:] | ||
|
|
||
| for line in lines { | ||
| // skip empty lines and unnamed symbols | ||
| if line.length == 0 || line.components(separatedBy: .whitespacesAndNewlines).filter({!$0.isEmpty}).count != 3 { | ||
| continue | ||
| } | ||
| let scanner = Scanner(string: line) | ||
|
|
||
| let startAddress = UInt32(scanner.scanUInt64(representation: .hexadecimal)!) | ||
| let symbolType = scanner.scanCharacter()! | ||
| let symbolName = scanner.scanUpToString("\n")! | ||
|
|
||
| if !include.contains(symbolType) { | ||
| continue | ||
| } | ||
| symbols[symbolName] = startAddress | ||
| } | ||
|
|
||
| return symbols | ||
| } | ||
|
|
||
| class func getDolphinMap() -> String { | ||
| let appSupportURL = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first | ||
| let appURLs = try? FileManager.default.contentsOfDirectory(at: appSupportURL!, includingPropertiesForKeys: nil) | ||
| let appURL = appURLs!.filter{ ["Dolphin", "dolphin-emu"].contains($0.lastPathComponent) }.first | ||
| return appURL!.appendingPathComponent("Maps/GXXE01.map").path | ||
| } | ||
|
|
||
| class func parseDolphinMap() -> [String:UInt32] { | ||
| let path = XGCompiler.getDolphinMap() | ||
| let text = try! String(contentsOfFile: path, encoding: String.Encoding.utf8) | ||
| let lines = text.components(separatedBy: CharacterSet.newlines) | ||
|
|
||
| var symbols: [String:UInt32] = [:] | ||
|
|
||
| for line in lines { | ||
| if line.first == "." || line.count == 0 { | ||
| continue | ||
| } | ||
| let charset = CharacterSet(charactersIn: ":/<>,'%?&") | ||
| if line.rangeOfCharacter(from: charset) != nil { | ||
| continue | ||
| } | ||
|
|
||
| let scanner = Scanner(string: line) | ||
|
|
||
| let startAddress = UInt32(scanner.scanUInt64(representation: .hexadecimal)!) | ||
| let _ /* symbolLength */ = scanner.scanUInt64(representation: .hexadecimal)! | ||
| let _ /* virtualAddress */ = scanner.scanUInt64(representation: .hexadecimal)! | ||
| let _ /* number */ = scanner.scanInt(representation: .decimal)! | ||
| let symbolName = String(scanner.string[scanner.currentIndex...]) | ||
|
|
||
| symbols[symbolName] = startAddress | ||
| } | ||
|
|
||
| return symbols | ||
| } | ||
|
|
||
| class func checkForCompiler() throws { | ||
|
|
||
| } | ||
|
|
||
| class func generateExternScript(_ externSymbols: [String:UInt32] = [:]) -> String { | ||
| var symbols = [String]() | ||
| for (symbol, address) in externSymbols { | ||
| symbols.append("\(symbol) = \(address.hexString());") | ||
| } | ||
|
|
||
| return symbols.joined(separator: "\n") | ||
| } | ||
|
|
||
| class func generateMapScript() -> String { | ||
| let symbolMap = parseDolphinMap() | ||
| return generateExternScript(symbolMap) | ||
| } | ||
|
|
||
| class func generateLinkerScript(offset: Int, name: String) -> String { | ||
| return """ | ||
| OUTPUT_FORMAT("elf32-powerpc", "elf32-powerpc", "elf32-powerpc") | ||
| OUTPUT_ARCH(powerpc:common) | ||
| ENTRY(\(name)) | ||
|
|
||
| SECTIONS { | ||
| . = \(offset.hexString()); | ||
|
|
||
| .text : { | ||
| *(.text) | ||
| } =0 | ||
|
|
||
| . = ALIGN(32); | ||
| .data : { | ||
| SORT(CONSTRUCTORS) | ||
| } | ||
| } | ||
| """ | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you want to keep this? I think I removed it and switched to the metroworks area in dol as the preference. This could still be useful in future if we end up writing a lot of assembly code. It's region specific though which is the main reason I didn't like it as a solution.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is actually just the
UInt32version of the function, they are identical with different return types