diff --git a/src/tools/cbindgen.cy b/src/tools/cbindgen.cy new file mode 100755 index 000000000..b34e639bb --- /dev/null +++ b/src/tools/cbindgen.cy @@ -0,0 +1,531 @@ +#!cyber +import os + +import clang 'clang_bs.cy' + +var POST_HEADER = " +" + +var args = os.parseArgs([ + -- Output cy path. + [ name: 'o', type: string, default: none ], + [ name: 'libpath', type: string, default: 'lib.dll' ], +]) + +var existingLibPath = false +var markerPos = none +my existing = none + +-- Determine where in the output file to emit generated bindings. +-- Also collect existing symbols that should be skipped. +if !isNone(args.o): + -- Build skip map. + existing = try os.readFile(args.o) catch '' + + markerPos = existing.find('\n-- CBINDGEN MARKER') + if isNone(markerPos): + markerPos = existing.len() + + var res = parseCyber(existing) + for res.decls -> decl: + if decl.pos < markerPos: + switch decl.type: + case 'func': + skipMap[decl.name] = true + case 'funcInit': + skipMap[decl.name] = true + case 'object': + skipMap[decl.name] = true + case 'variable': + if decl.name == 'Root.libPath': + existingLibPath = true + +if args.rest.len() <= 2: + print 'Missing path to header file.' + os.exit(1) + +my headerPath = args.rest[2] + +var headerSrc = os.readFile(headerPath) +headerSrc = POST_HEADER + headerSrc + +var unit = getTranslationUnit(headerPath) + +for 0..clang.lib.clang_getNumDiagnostics(unit) -> i: + var diag = clang.lib.clang_getDiagnostic(unit, i) + my spelling = clang.lib.clang_getDiagnosticSpelling(diag) + print os.fromCstr(spelling).decode() + +my cursor = clang.lib.clang_getTranslationUnitCursor(unit) + +skipMap['__gnuc_va_list'] = true +skipMap['va_list'] = true +skipMap['true'] = true +skipMap['false'] = true + +var state = [State type: .root] +var cstate = clang.ffi.bindObjPtr(state) + +out += "-- Code below is generated by cbindgen.cy\n" + +-- enum CXChildVisitResult(*CXCursorVisitor) (CXCursor cursor, CXCursor parent, CXClientData client_data) +cvisitor = clang.ffi.bindCallback(visitor, [clang.CXCursor, clang.CXCursor, .voidPtr], .int) + +clang.lib.clang_visitChildren(cursor, cvisitor, cstate) + +-- Generate ffi init. +out += "\nimport os\n" +out += "my Root.lib = load()\n" +out += "func load():\n" +out += " var ffi = os.newFFI()\n" +for structs -> name: + var fieldTypes = structMap[name].fieldTypes + out += " ffi.cbind($(name), [$(fieldTypes.joinString(', '))])\n" +for funcs -> fn: + out += " ffi.cfunc('$(fn.name)', [$(fn.params.joinString(', '))], $(fn.ret))\n" +var libPath = existingLibPath ? 'libPath' else "'$(args.libpath)'" +-- var libPath = 'libPath' +out += " my lib = ffi.bindLib($(libPath), [genMap: true])\n" + +-- Reassign to static funcs. +for funcs -> fn: + if skipMap[fn.name]: + out += '-- ' + out += " $(fn.name) = lib.$(fn.name)\n" + +out += " return lib\n\n" + +-- Generate macros. +genMacros(headerPath) + +-- Final output. +if !isNone(args.o): + out = existing[0..markerPos] + '\n-- CBINDGEN MARKER\n' + out + +os.writeFile('bindings.cy', out) + +-- Declarations. + +var Root.skipMap = [:] +var Root.macros = [] +var Root.cvisitor = none +-- Build output string. +var Root.out = '' +var Root.skipChildren = false +var Root.aliases = [:] -- aliasName -> structName or binding symbol (eg: .voidPtr) +my Root.struct = none +var Root.structMap = [:] -- structName -> list of fields (symOrName) +var Root.enumMap = [:] +var Root.structs = [] +var Root.funcs = [] +-- var Root.arrays = [:] -- [n]typeName -> true +-- var vars = [:] -- varName -> bindingType + +func getTranslationUnit(headerPath): + var args = os.malloc(8 * 1) + -- args.writeAt(0, os.cstr('-isysroot')) + -- args.writeAt(8, os.cstr('/Library/Developer/CommandLineTools/SDKs/MacOSX13.sdk')) + args.writeAt(0, os.cstr('-I/opt/homebrew/Cellar/llvm/17.0.5/lib/clang/17/include')) + + var cpath = os.cstr(headerPath) + var index = clang.lib.clang_createIndex(0, 0) + return clang.lib.clang_parseTranslationUnit(index, cpath, args, 1, none, 0, + -- clang.CXTranslationUnit_DetailedPreprocessingRecord | clang.CXTranslationUnit_SkipFunctionBodies | clang.CXTranslationUnit_SingleFileParse) + clang.CXTranslationUnit_DetailedPreprocessingRecord | clang.CXTranslationUnit_SkipFunctionBodies) + +func getMacrosTranslationUnit(hppPath): + var args = os.malloc(8 * 1) + args.writeAt(0, os.cstr('-I/opt/homebrew/Cellar/llvm/17.0.5/lib/clang/17/include')) + + var cpath = os.cstr(hppPath) + var index = clang.lib.clang_createIndex(0, 0) + return clang.lib.clang_parseTranslationUnit(index, cpath, args, 1, none, 0, + clang.CXTranslationUnit_SkipFunctionBodies) + +type Struct object: + var fieldTypes List + var fieldNames List + var cxFieldTypes List + +type StateType enum: + case root + case struct + case enum + case macrosRoot + case initVar + case initListExpr + +type State object: + var type StateType + my data + +func visitor(cursor, parent, client_data): + var state State = client_data.asObject() + switch state.type: + case StateType.root: + return rootVisitor(cursor, parent, state) + case StateType.struct: + return structVisitor(cursor, parent, state) + case StateType.enum: + return enumVisitor(cursor, parent, state) + case StateType.macrosRoot: + return macrosRootVisitor(cursor, parent, state) + case StateType.initVar: + return initVarVisitor(cursor, parent, state) + case StateType.initListExpr: + return initListExpr(cursor, parent, state) + else: + throw error.Unsupported + +func rootVisitor(cursor, parent, state): + var cxName = clang.lib.clang_getCursorDisplayName(cursor) + var name = fromCXString(cxName) + + var loc = clang.lib.clang_getCursorLocation(cursor) + if clang.lib.clang_Location_isInSystemHeader(loc) != 0: + -- Skip system headers. + return clang.CXChildVisit_Continue + + switch cursor.kind: + case clang.CXCursor_MacroDefinition: + if clang.lib.clang_Cursor_isMacroBuiltin(cursor) != 0: + return clang.CXChildVisit_Continue + if clang.lib.clang_Cursor_isMacroFunctionLike(cursor) != 0: + return clang.CXChildVisit_Continue + + -- Append to macros. + macros.append(name) + + case clang.CXCursor_MacroExpansion, + clang.CXCursor_InclusionDirective: pass + + case clang.CXCursor_TypedefDecl: + -- print 'typedef $(name)' + if skipMap[name]: + out += '-- typedef $(name)\n\n' + else: + var atype = clang.lib.clang_getTypedefDeclUnderlyingType(cursor) + var bindType = toBindType(atype) + if typeof(bindType) == symbol or bindType != name: + aliases[name] = bindType + out += 'type $(name) $(toCyType(bindType, true))\n\n' + + case clang.CXCursor_StructDecl: + -- print 'struct $(name)' + + struct = [Struct fieldTypes: [], fieldNames: [], cxFieldTypes: []] + structMap[name] = struct + + var newState = [State type: .struct] + var cnewState = clang.ffi.bindObjPtr(newState) + clang.lib.clang_visitChildren(cursor, cvisitor, cnewState) + clang.ffi.unbindObjPtr(newState) + + if struct.fieldTypes.len() == 0: + -- Empty struct, skip. + return clang.CXChildVisit_Continue + + structs.append(name) + if skipMap[name]: + out += '-- type $(name) object:\n' + skipChildren = true + else: + out += 'type $(name) object:\n' + + for struct.fieldNames -> name, i: + if skipChildren: + out += '-- ' + + var fieldt = struct.fieldTypes[i] + out += ' var $(name) $(toCyType(fieldt, false))' + if is(fieldt, .voidPtr) or + (typeof(fieldt) == string and fieldt.startsWith('[os.CArray')): + out += ' -- $(struct.cxFieldTypes[i])' + out += '\n' + + out += '\n' + skipChildren = false + + case clang.CXCursor_EnumDecl: + -- print 'enum $(name)' + + aliases[name] = .int + + var newState = [State type: .enum] + var cnewState = clang.ffi.bindObjPtr(newState) + clang.lib.clang_visitChildren(cursor, cvisitor, cnewState) + clang.ffi.unbindObjPtr(newState) + out += '\n' + + case clang.CXCursor_FunctionDecl: + -- print 'func $(name)' + + var cxName = clang.lib.clang_getCursorSpelling(cursor) + var funcName = fromCXString(cxName) + var fn = [:] + fn['name'] = funcName + + var cxFunc = clang.lib.clang_getCursorType(cursor) + var cxRet = clang.lib.clang_getResultType(cxFunc) + + var outFunc = 'func $(funcName)(' + + -- Parse params. + var fnParamTypes = [] + var numParams = clang.lib.clang_getNumArgTypes(cxFunc) + for 0..numParams -> i: + var cxParam = clang.lib.clang_Cursor_getArgument(cursor, i) + var cxParamName = clang.lib.clang_getCursorSpelling(cxParam) + var paramName = fromCXString(cxParamName) + var cxParamType = clang.lib.clang_getArgType(cxFunc, i) + var paramT = toBindType(cxParamType) + + outFunc += '$(paramName) $(toCyType(paramT, false))' + if i < numParams-1: + outFunc += ', ' + + fnParamTypes.append(paramT) + + outFunc += ') ' + + var retT = toBindType(cxRet) + outFunc += '$(toCyType(retT, true)): pass' + + if skipMap[funcName]: + outFunc = '-- ' + outFunc + + out += '$(outFunc)\n' + + fn['params'] = fnParamTypes + fn['ret'] = retT + funcs.append(fn) + + case clang.CXCursor_VarDecl: + print 'TODO: var $(name)' + + else: + print 'visitor invoked $(cursor.kind) $(name)' + throw error.Unsupported + return clang.CXChildVisit_Continue + +func structVisitor(cursor, parent, state): + var cxName = clang.lib.clang_getCursorDisplayName(cursor) + var name = fromCXString(cxName) + + switch cursor.kind: + case clang.CXCursor_FieldDecl: + -- print 'field $(cursor.kind) $(name)' + my ftype = clang.lib.clang_getCursorType(cursor) + var fsym = toBindType(ftype) + + struct.fieldTypes.append(fsym) + struct.fieldNames.append(name) + + var cxTypeName = clang.lib.clang_getTypeSpelling(ftype) + var typeName = fromCXString(cxTypeName) + struct.cxFieldTypes.append(typeName) + else: + print 'unsupported $(cursor.kind) $(name)' + return clang.CXChildVisit_Continue + +func enumVisitor(cursor, parent, state): + var cxName = clang.lib.clang_getCursorDisplayName(cursor) + var name = fromCXString(cxName) + var val = clang.lib.clang_getEnumConstantDeclValue(cursor) + + out += 'var Root.$(name) int = $(val)\n' + return clang.CXChildVisit_Continue + +func genMacros(headerPath): + var absPath = os.realPath(headerPath) + + var hpp = '' + hpp += '#include "$(absPath)"\n\n' + for macros -> macro: + hpp += 'auto var_$(macro) = $(macro);\n' + os.writeFile('macros.hpp', hpp) + + var unit = getMacrosTranslationUnit('macros.hpp') + my cursor = clang.lib.clang_getTranslationUnitCursor(unit) + + for 0..clang.lib.clang_getNumDiagnostics(unit) -> i: + var diag = clang.lib.clang_getDiagnostic(unit, i) + my spelling = clang.lib.clang_getDiagnosticSpelling(diag) + print os.fromCstr(spelling).decode() + + out += "-- Macros\n" + + var state = [State type: .macrosRoot] + var cstate = clang.ffi.bindObjPtr(state) + clang.lib.clang_visitChildren(cursor, cvisitor, cstate) + +func initListExpr(cursor, parent, state): + switch cursor.kind: + case clang.CXCursor_IntegerLiteral: + var eval = clang.lib.clang_Cursor_Evaluate(cursor) + var val = clang.lib.clang_EvalResult_getAsLongLong(eval) + state.data.append(val) + else: + print 'visitor invoked $(cursor.kind)' + throw error.Unsupported + + return clang.CXChildVisit_Continue + +func initVarVisitor(cursor, parent, state): + var cxName = clang.lib.clang_getCursorDisplayName(cursor) + var name = fromCXString(cxName) + + switch cursor.kind: + case clang.CXCursor_TypeRef: + state.data['type'] = name + case clang.CXCursor_InitListExpr: + var args = [] + var newState = [State type: .initListExpr, data: args] + var cnewState = clang.ffi.bindObjPtr(newState) + clang.lib.clang_visitChildren(cursor, cvisitor, cnewState) + clang.ffi.unbindObjPtr(newState) + state.data['args'] = args + else: + print 'visitor invoked $(cursor.kind) $(name)' + throw error.Unsupported + + return clang.CXChildVisit_Continue + +func macrosRootVisitor(cursor, parent, state): + var cxName = clang.lib.clang_getCursorDisplayName(cursor) + var name = fromCXString(cxName) + + var loc = clang.lib.clang_getCursorLocation(cursor) + if clang.lib.clang_Location_isInSystemHeader(loc) != 0: + -- Skip system headers. + return clang.CXChildVisit_Continue + + switch cursor.kind: + case clang.CXCursor_UnexposedDecl: pass + case clang.CXCursor_MacroDefinition: pass + case clang.CXCursor_MacroExpansion, + clang.CXCursor_InclusionDirective: pass + case clang.CXCursor_TypedefDecl: pass + case clang.CXCursor_StructDecl: pass + case clang.CXCursor_EnumDecl: pass + case clang.CXCursor_FunctionDecl: pass + case clang.CXCursor_VarDecl: + if !name.startsWith('var_'): + -- Skip non-macro vars. + return clang.CXChildVisit_Continue + + if skipMap[name[4..]]: + out += '-- ' + + -- print 'var $(name)' + var eval = clang.lib.clang_Cursor_Evaluate(cursor) + var kind = clang.lib.clang_EvalResult_getKind(eval) + + var finalName = name[4..].trim(.left, '_') + switch kind: + case clang.CXEval_UnExposed: + -- Can't eval to primitive. Check for struct intializer. + my initCur = clang.lib.clang_Cursor_getVarDeclInitializer(cursor) + + var cxInitName = clang.lib.clang_getCursorDisplayName(initCur) + var initName = fromCXString(cxInitName) + switch initCur.kind: + case clang.CXCursor_InvalidFile: pass + case clang.CXCursor_CXXFunctionalCastExpr: + var state = [State type: .initVar, data: [:]] + var cstate = clang.ffi.bindObjPtr(state) + clang.lib.clang_visitChildren(initCur, cvisitor, cstate) + clang.ffi.unbindObjPtr(state) + + var initT = state.data.type + my struct = structMap[initT] + var kvs = [] + for struct.fieldNames -> fieldn, i: + kvs.append('$(fieldn): $(state.data.args[i])') + out += 'var Root.$(finalName) $(initT) = [$(initT) $(kvs.joinString(", "))]\n' + else: + print 'init $(initName) $(initCur.kind)' + throw error.Unsupported + case clang.CXEval_Int: + var val = clang.lib.clang_EvalResult_getAsLongLong(eval) + out += 'var Root.$(finalName) int = $(val)\n' + case clang.CXEval_Float: + var val = clang.lib.clang_EvalResult_getAsDouble(eval) + out += 'var Root.$(finalName) float = $(val)\n' + case clang.CXEval_StrLiteral: + my strz = clang.lib.clang_EvalResult_getAsStr(eval) + var str = os.fromCstr(strz).decode() + out += 'var Root.$(finalName) string = "$(str)"\n' + else: + print '$(kind)' + throw error.Unsupported + + clang.lib.clang_EvalResult_dispose(eval) + else: + print 'visitor invoked $(cursor.kind) $(name)' + throw error.Unsupported + return clang.CXChildVisit_Continue + +func fromCXString(cxStr) string: + my cname = clang.lib.clang_getCString(cxStr) + return os.fromCstr(cname).decode() + +func toCyType(nameOrSym, forRet): + if typeof(nameOrSym) == symbol: + switch nameOrSym: + case .voidPtr : return forRet ? 'pointer' else 'any' -- `any` until Optionals are done + case .bool : return 'bool' + case .int : return 'int' + case .uint : return 'int' + case .char : return 'int' + case .uchar : return 'int' + case .long : return 'int' + case .float : return 'float' + case .double : return 'float' + case .voidPtr : return 'pointer' + case .void : return 'none' + else: + print 'Unsupported type $(nameOrSym)' + throw error.Unsupported + else: + if nameOrSym.startsWith('[os.CArray'): + return 'List' + return nameOrSym + +func toBindType(cxType): + switch cxType.kind: + case clang.CXType_Float : return .float + case clang.CXType_Double : return .double + case clang.CXType_Long : return .long + case clang.CXType_Void : return .void + case clang.CXType_Bool : return .bool + case clang.CXType_Int : return .int + case clang.CXType_UInt : return .uint + case clang.CXType_Char_S : return .char + case clang.CXType_UChar : return .uchar + case clang.CXType_Pointer : return .voidPtr + case clang.CXType_Elaborated: + var decl = clang.lib.clang_getTypeDeclaration(cxType) + var declName = clang.lib.clang_getCursorDisplayName(decl) + my declNameC = clang.lib.clang_getCString(declName) + var name = os.fromCstr(declNameC).decode() + + if structMap[name]: + -- Valid type. + return name + else aliases[name]: + -- Valid alias. + return aliases[name] + else enumMap[name]: + return name + + print 'Unsupported elaborated type: $(name)' + throw error.Unsupported + case clang.CXType_ConstantArray: + var n = clang.lib.clang_getNumElements(cxType) + var cxElem = clang.lib.clang_getElementType(cxType) + var elem = toBindType(cxElem) + return '[os.CArray n: $(n), elem: $(elem)]' + else: + print 'Unsupported type $(cxType.kind)' + throw error.Unsupported \ No newline at end of file