├── .gitignore ├── README.md └── SwiftingBridge.py /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | *.swift 3 | 4 | *.h 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftingBridge 2 | 3 | Apple's Scripting Bridge is totally awesome, but also unfortunately a complete mess. Swift is also fantastic, but still a bit of a mess as it continues to evolve. Here I have created their bastard child. 4 | 5 | Swifting Bridge uses Apple's builtin `sdef` and `sdp` utilities to generate a Scripting Bridge header (as described in [Apple's official documentation for Scripting Bridge](https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ScriptingBridgeConcepts/UsingScriptingBridge/UsingScriptingBridge.html#//apple_ref/doc/uid/TP40006104-CH4-SW1)) and then uses that generated Objective-C header to generate a native Swift file with (theoretically) all of the same functionality. That Swift file can be drug straight into a full Swift application and voila. 6 | 7 | - No Objective-C Bridging Headers. 8 | - No Swift wrappers. 9 | - No dealing with generic `SBObject`s. 10 | - No damn linker errors. 11 | -------------------------------------------------------------------------------- /SwiftingBridge.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import subprocess 3 | from os import devnull 4 | 5 | out = None 6 | f = None 7 | definedTypes = [] 8 | 9 | def generateHeader(appPath, appName): 10 | if appPath[-1:] == "/": 11 | appPath = appPath[:-1] 12 | 13 | if appPath[-4:] != ".app": 14 | print "Invalid application path" 15 | exit() 16 | 17 | FNULL = open(devnull, "w") 18 | 19 | sdef = subprocess.Popen(["sdef", appPath], stdout=subprocess.PIPE) 20 | sdp = subprocess.Popen(["sdp", "-fh", "--basename", appName], stdin=sdef.stdout, stderr=FNULL) 21 | sdp.communicate() 22 | 23 | def isComment(line): 24 | if line[:2] == "//": 25 | out.write(line) 26 | return True 27 | 28 | elif line[:2] == "/*": 29 | try: 30 | out.write(line[line.index("/*"):line.index("*/")+2]) 31 | 32 | except ValueError: 33 | out.write(line[line.index("/*"):]) 34 | 35 | 36 | while not "*/" in line: 37 | line = f.readline() 38 | out.write(line) 39 | 40 | return True 41 | 42 | def isImport(line): 43 | if line[:7] == "#import": 44 | frameworkName = line[9:line.index("/")] 45 | out.write("import " + frameworkName + "\n") 46 | return True 47 | 48 | def isEnum(line): 49 | if line[:4] == "enum": 50 | enumName = line[5:line.index(" {")] 51 | out.write("@objc enum " + enumName + ": ") 52 | 53 | line = f.readline() 54 | if "'" in line: 55 | out.write("NSInteger ") 56 | else: 57 | print "Warning: Must give type to enum " + enumName 58 | 59 | out.write("{\n") 60 | while not "};" in line: 61 | line = line.replace(",", "") #remove trailing commas 62 | 63 | if "'" in line: 64 | text = line[line.find("'") + 1:line.rfind("'")] 65 | out.write("\tcase ") 66 | out.write(line[1:line.find(" = ") + 3]) 67 | out.write("0x" + text.encode("hex")) 68 | out.write(line[line.rfind("'")+1:]) 69 | else: 70 | if " = " in line: 71 | out.write("\tcase ") 72 | out.write(line[1:]) 73 | else: 74 | out.write(line) 75 | line = f.readline() 76 | 77 | out.write(line) 78 | 79 | return True 80 | 81 | def isInterface(line): 82 | global definedTypes 83 | if line[:10] == "@interface": 84 | interfaceName = line[11:line[11:].index(" ") + 11] 85 | 86 | out.write("@objc protocol " + interfaceName) 87 | try: 88 | super = line[line.index(":")+2:-1] 89 | if super in definedTypes: 90 | out.write(": " + super) 91 | except ValueError: 92 | super = None 93 | 94 | out.write(" {\n") 95 | 96 | if interfaceName in definedTypes: 97 | print "Warning: protocol for type '" + interfaceName + "' defined multiple times" 98 | 99 | definedTypes.append(interfaceName) 100 | 101 | line = f.readline() 102 | while not "@end" in line: 103 | handleLine(line) 104 | line = f.readline() 105 | 106 | out.write("}\n") 107 | 108 | if super != "SBApplication": #Assume everything else inherits from SBObject 109 | out.write("extension SBObject: " + interfaceName + "{}\n") 110 | 111 | else: 112 | out.write("extension SBApplication: " + interfaceName + "{}\n") 113 | 114 | return True 115 | 116 | def parseType(type): 117 | type = type.rstrip("* ") 118 | type = escapedName(type) 119 | 120 | typeDict = { 121 | "id": "AnyObject", 122 | "BOOL": "Bool", 123 | "bool": "CBool", 124 | "char": "CChar", 125 | "signed char": "CChar", 126 | "unsigned char": "CUnsignedChar", 127 | "short": "CShort", 128 | "unsigned short": "CUnsignedShort", 129 | "int": "CInt", 130 | "unsigned int": "CUnsignedInt", 131 | "long": "CLong", 132 | "unsigned long": "CUnsignedLong", 133 | "long long": "CLongLong", 134 | "unsigned long long": "CUnsignedLongLong", 135 | "wchar_t": "CWideChar", 136 | "char16_t": "CChar16", 137 | "char32_t": "CChar32", 138 | "float": "CFloat", 139 | "double": "CDouble" 140 | } 141 | 142 | try: 143 | return typeDict[type] 144 | 145 | except KeyError: 146 | return type 147 | 148 | def escapedName(name): 149 | 150 | #Objective-C escaped keywords end in _ 151 | #We must un-escape them first 152 | if name[-1:] == "_": 153 | name = name[:-1] 154 | 155 | reservedWords = ["as", "in", "for", "class", "deinit", "enum", "extension", "func", "import", "init", "internal", "let", "operator", "private", "protocol", "public", "static", "struct", "subscript", "typealias", "var", "break", "case", "continue", "default", "do", "else", "fallthrough", "for", "if", "in", "return", "switch", "where", "while", "as", "dynamicType", "false", "is", "nil", "self", "Self", "super", "true", " associativity", "convenience", "dynamic", "didSet", "final", "get", "infix", "inout", "lazy", "left", "mutating", "none", "nonmutating", "optional", "override", "postfix", "precedence", "prefix", "Protocol", "required", "right", "set", "Type", "unowned", "weak", "willSet"] 156 | 157 | if name in reservedWords: 158 | name = "`" + name + "`" 159 | 160 | return name 161 | 162 | def isProperty(line): 163 | if line[:9] == "@property": 164 | typeIndex = 10 165 | readonly = False 166 | 167 | try: 168 | attrs = line[line.index("(")+1:line.index(")")] #TODO: Split by ", " 169 | typeIndex = line.index(")") + 2 170 | 171 | #TODO: Anything but this 172 | #not proud of this, but it works 173 | attrs.index("readonly") #if this throws a ValueError, then the next line won't run 174 | readonly = True 175 | 176 | except ValueError: 177 | attrs = None 178 | 179 | nameIndex = line.rfind(" ") 180 | name = line[nameIndex + 1:] 181 | 182 | #handle pointers 183 | if name[0] == "*": 184 | name = name[1:] 185 | 186 | type = line[line[:nameIndex].rfind(" ")+1:nameIndex] 187 | type = parseType(type) 188 | 189 | out.write("\toptional var " + name + ": " + type + " {get") 190 | 191 | if readonly: 192 | out.write("}\n") 193 | 194 | else: 195 | out.write(" set}\n") 196 | 197 | return True 198 | 199 | def isFunction(line): 200 | if line[:3] == "- (": 201 | 202 | returnTypeEnd = line.index(")") 203 | returnType = line[3:returnTypeEnd] 204 | returnType = parseType(returnType) 205 | 206 | line = line[returnTypeEnd + 2:] 207 | 208 | inparams = line.split(":") 209 | funcName = inparams.pop(0) 210 | outparams = [] 211 | 212 | 213 | for param in inparams: 214 | type = param[param.index("(")+1:param.index(")")] 215 | param = param[len(type)+2:] 216 | 217 | 218 | type = parseType(type) 219 | 220 | name = escapedName(param.split(" ")[0]) 221 | 222 | outparams.append((name, type)) 223 | 224 | out.write("\toptional func " + funcName + "(") 225 | 226 | if len(outparams) > 0: 227 | (param, type) = outparams.pop(0) 228 | out.write(param + ": " + type) #handles that pesky comma issue 229 | 230 | for (param, type) in outparams: 231 | out.write(", " + param + ": " + type) 232 | 233 | if returnType != "void": 234 | out.write(") -> " + returnType + "\n") 235 | 236 | else: 237 | out.write(")\n") 238 | 239 | return True 240 | 241 | def isEmptyLine(line): 242 | if line.rstrip() == "": 243 | return True 244 | 245 | def isJunkLine(line): 246 | if line[:6] == "@class": 247 | return True 248 | 249 | if line[:12] == "typedef enum": 250 | return True 251 | 252 | def handleLine(line): 253 | line = line.replace("'", '"') #replace single quotes with double quotes 254 | line = line.lstrip() #remove whitespace 255 | statements = line.split(";") #in case of statements (including code w/ comments) on single line 256 | 257 | 258 | if not isComment(statements[0]): 259 | if not isImport(statements[0]): 260 | if not isEnum(statements[0]): 261 | if not isInterface(statements[0]): 262 | if not isProperty(statements[0]): 263 | if not isFunction(statements[0]): 264 | if not isEmptyLine(statements[0]): 265 | if not isJunkLine(statements[0]): 266 | print "Here there be dragons:" 267 | print line 268 | 269 | #handle any extra statements in same line 270 | if len(statements) > 1: 271 | handleLine(statements[1]) 272 | 273 | 274 | if len(sys.argv) <= 1: 275 | appPath = raw_input("Application path: ") 276 | appName = raw_input("Application name: ") 277 | 278 | elif len(sys.argv) == 3: 279 | appPath = sys.argv[1] 280 | appName = sys.argv[2] 281 | 282 | else: 283 | print "Invalid number of arguments" 284 | 285 | generateHeader(appPath, appName) 286 | out = open(appName + ".swift", "w") 287 | 288 | 289 | f = open(appName + ".h") 290 | 291 | #this allows handleLine to process multiple lines in a single call 292 | lastLine = f.readline() 293 | while lastLine != "": 294 | handleLine(lastLine) 295 | lastLine = f.readline() 296 | 297 | f.close() 298 | out.close() --------------------------------------------------------------------------------