#!LUA50 -- -*- mode: lua -*- -- ion/mkexports.lua -- -- Copyright (c) Tuomo Valkonen 2003-2005. -- -- Ion is free software; you can redistribute it and/or modify it under -- the terms of the GNU Lesser General Public License as published by -- the Free Software Foundation; either version 2.1 of the License, or -- (at your option) any later version. -- -- -- This is a script to automatically generate exported function registration -- code and documentation for those from C source. -- -- The script can also parse documentation comments from Lua code. -- -- Helper functions {{{ function errorf(fmt, ...) error(string.format(fmt, unpack(arg)), 2) end function matcherr(s) error(string.format("Parse error in \"%s...\"", string.sub(s, 1, 50)), 2) end function fprintf(h, fmt, ...) h:write(string.format(fmt, unpack(arg))) end function trim(str) return string.gsub(str, "^[%s\n]*(.-)[%s\n]*$", "%1") end -- }}} -- Some conversion tables {{{ desc2ct={ ["v"]="void", ["i"]="int", ["d"]="double", ["b"]="bool", ["t"]="ExtlTab", ["f"]="ExtlFn", ["o"]="Obj*", ["s"]="char*", ["S"]="const char*", } ct2desc={ ["uint"] = "i", } for d, t in pairs(desc2ct) do ct2desc[t]=d end desc2human={ ["v"]="void", ["i"]="integer", ["d"]="double", ["b"]="bool", ["t"]="table", ["f"]="function", ["o"]="object", ["s"]="string", ["S"]="string", ["a"]="any value", } -- }}} -- Parser {{{ local classes={} local chnds={} local reexports={} function add_chnd(fnt) local odesc=string.gsub(fnt.odesc, "S", "s") local idesc=string.gsub(fnt.idesc, "S", "s") local str="l2chnd_" .. odesc .. "_" .. idesc .. "_" for i, t in ipairs(fnt.itypes) do str=str .. "_" .. t end chnds[str]={odesc=odesc, idesc=idesc, itypes=fnt.itypes} fnt.chnd=str end function add_class(cls) if cls~="Obj" and not classes[cls] then classes[cls]={} end end function sort_classes(cls) local sorted={} local inserted={} local function insert(cls) if classes[cls] and not inserted[cls] then if classes[cls].parent then insert(classes[cls].parent) end inserted[cls]=true table.insert(sorted, cls) end end for cls in pairs(classes) do insert(cls) end return sorted end function parse_type(t) local desc, otype, varname="?", "", "" -- Remove whitespace at start end end of the string and compress elsewhere. t=string.gsub(trim(t), "[%s\n]+", " ") -- Remove spaces around asterisks. t=string.gsub(t, " *%* *", "*") -- Add space after asterisks. t=string.gsub(t, "%*", "* ") -- Check for const local is_const="" local s, e=string.find(t, "^const +") if s then is_const="const " t=string.sub(t, e+1) end -- Find variable name part tn=t s, e=string.find(tn, " ") if s then varname=string.sub(tn, e+1) tn=string.sub(tn, 1, s-1) assert(not string.find(varname, " ")) end -- Try to check for supported types desc = ct2desc[is_const .. tn] if not desc or desc=="o" then s, e=string.find(tn, "^[A-Z][%w_]*%*$") if s then desc="o" otype=string.sub(tn, s, e-1) add_class(otype) else errorf("Error parsing type from \"%s\"", t) end end return desc, otype, varname end function parse(d) local doc=nil local safe=false local untraced=false -- Handle /*EXTL_DOC ... */ local function do_doc(s) --s=string.gsub(s, "/%*EXTL_DOC(.-)%*/", "%1") local st=string.len("/*EXTL_DOC") local en, _=string.find(s, "%*/") if not en then errorf("Could not find end of comment in \"%s...\"", string.sub(s, 1, 50)) end s=string.sub(s, st+1, en-1) s=string.gsub(s, "\n[%s]*%*", "\n") doc=s end -- Handle EXTL_SAFE local function do_safe(s) assert(not safe) safe=true end -- Handle EXTL_UNTRACED local function do_untraced(s) assert(not untraced) untraced=true end local function do_do_export(cls, efn, ot, fn, param) local odesc, otype=parse_type(ot) local idesc, itypes, ivars="", {}, {} -- Parse arguments param=string.sub(param, 2, -2) if string.find(param, "[()]") then errorf("Error: parameters to %s contain parantheses", fn) end param=trim(param) if string.len(param)>0 then for p in string.gfind(param .. ",", "([^,]*),") do local spec, objtype, varname=parse_type(p) idesc=idesc .. spec table.insert(itypes, objtype) table.insert(ivars, varname) end end if cls=="?" then if string.sub(idesc, 1, 1)~="o" then error("Invalid class for " .. fn) end cls=itypes[1] end -- Generate call handler name local fninfo={ doc=doc, safe=safe, untraced=untraced, odesc=odesc, otype=otype, idesc=idesc, itypes=itypes, ivars=ivars, exported_name=efn, class=cls, } add_chnd(fninfo) add_class(cls) if not classes[cls].fns then classes[cls].fns={} end assert(not classes[cls].fns[fn], "Function " .. fn .. " multiply defined!") classes[cls].fns[fn]=fninfo -- Reset doc=nil safe=false untraced=false end -- Handle EXTL_EXPORT otype fn(args) local function do_export(s) local mdl, efn local pat="EXTL_EXPORT[%s\n]+([%w%s_*]+[%s\n*])([%w_]+)[%s\n]*(%b())" local st, en, ot, fn, param=string.find(s, pat) if not st then matcherr(s) end if module=="global" or not module then efn=fn mdl=module else st, en, efn=string.find(fn, "^"..module.."_(.*)") if efn then mdl=module else for k in pairs(reexports) do st, en, efn=string.find(fn, "^"..k.."_(.*)") if efn then mdl=module break end end end if not mdl then error('"'..fn..'" is not a valid function name of format '.. 'modulename_fnname.') end end do_do_export(module, efn, ot, fn, param) end -- Handle EXTL_EXPORT_MEMBER otype prefix_fn(class, args) local function do_export_member(s) local pat="EXTL_EXPORT_MEMBER[%s\n]+([%w%s_*]+[%s\n*])([%w_]+)[%s\n]*(%b())" local st, en, ot, fn, param=string.find(s, pat) if not st then matcherr(s) end local efn=string.gsub(fn, ".-_(.*)", "%1") do_do_export("?", efn, ot, fn, param) end -- Handle EXTL_EXPORT_AS(table, member_fn) otype fn(args) local function do_export_as(s) local pat="EXTL_EXPORT_AS%(%s*([%w_]+)%s*,%s*([%w_]+)%s*%)[%s\n]+([%w%s_*]+[%s\n*])([%w_]+)[%s\n]*(%b())" local st, en, cls, efn, ot, fn, param=string.find(s, pat) if not st then matcherr(s) end do_do_export((reexports[cls] and module or cls), efn, ot, fn, param) end local function do_implobj(s) local pat="IMPLCLASS%(%s*([%w_]+)%s*,%s*([%w_]+)%s*,[^)]*%)" local st, en, cls, par=string.find(s, pat) if not st then matcherr(s) end add_class(cls) classes[cls].parent=par end local function do_class(s) local pat="EXTL_CLASS%(%s*([%w_]+)%s*,%s*([%w_]+)%s*%)" local st, en, cls, par=string.find(s, pat) if not st then matcherr(s) end add_class(cls) classes[cls].parent=par end local lookfor={ {"/%*EXTL_DOC", do_doc}, {"[%s\n]EXTL_SAFE[%s\n]", do_safe}, {"[%s\n]EXTL_UNTRACED[%s\n]", do_untraced}, {"[%s\n]EXTL_EXPORT[%s\n]+IMPLCLASS", do_implobj}, {"[%s\n]EXTL_EXPORT[%s\n]", do_export}, {"[%s\n]EXTL_EXPORT_AS", do_export_as}, {"[%s\n]EXTL_EXPORT_MEMBER[%s\n]", do_export_member}, {"[%s\n]EXTL_CLASS", do_class}, } do_parse(d, lookfor) end function do_parse(d, lookfor) while true do local mins, mine, minfn=string.len(d)+1, nil, nil for _, lf in ipairs(lookfor) do local s, e=string.find(d, lf[1]) if s and s #include ]]) -- end blockwrite -- Write class infos and check that the class is implemented in the -- module. for c, data in pairs(classes) do if string.lower(c)==c then data.module=true else fprintf(h, "EXTL_DEFCLASS(%s);\n", c) if data.fns and not data.parent then error(c..": Methods can only be registered if the class " .. "is implemented in the module in question.") end end end -- Write L2 call handlers for name, info in pairs(chnds) do writechnd(h, name, info) end fprintf(h, "\n") for cls, data in pairs(classes) do if data.fns then -- Write function declarations for fn in pairs(data.fns) do fprintf(h, "extern void %s();\n", fn) end -- Write function table write_class_fns(h, cls, data) else fprintf(h, "#define %s_exports NULL\n", cls) end end fprintf(h, "bool %sregister_exports()\n{\n", pfx(module)) local sorted_classes=sort_classes() for _, cls in pairs(sorted_classes) do if cls=="global" then fprintf(h, " if(!extl_register_functions(global_exports)) return FALSE;\n") elseif classes[cls].module then fprintf(h, " if(!extl_register_module(\"%s\", %s_exports)) return FALSE;\n", cls, cls) elseif classes[cls].parent then fprintf(h, " if(!extl_register_class(\"%s\", %s_exports, \"%s\")) return FALSE;\n", cls, cls, classes[cls].parent) end end fprintf(h, " return TRUE;\n}\n\nvoid %sunregister_exports()\n{\n", pfx(module)) for _, cls in pairs(sorted_classes) do if cls=="global" then fprintf(h, " extl_unregister_functions(global_exports);\n") elseif classes[cls].module then fprintf(h, " extl_unregister_module(\"%s\", %s_exports);\n", cls, cls) elseif classes[cls].parent then fprintf(h, " extl_unregister_class(\"%s\", %s_exports);\n", cls, cls) end end fprintf(h, "}\n\n") end function write_header(h) local p=pfx(module) local u=string.upper(p) fprintf(h, [[ /* Automatically generated by mkexports.lua */ #ifndef %sEXTL_EXPORTS_H #define %sEXTL_EXPORTS_H #include extern bool %sregister_exports(); extern void %sunregister_exports(); #endif /* %sEXTL_EXPORTS_H */ ]], u, u, p, p, u) end -- }}} -- Documentation output {{{ function tohuman(desc, objtype) if objtype~="" then return objtype else return desc2human[desc] end end function texfriendly(name) return string.gsub(name, "_", "-") end function texfriendly_typeormod(nm) if string.find(nm, "A-Z") then return "\\type{"..string.gsub(nm, '_', '\_').."}" else return "\\code{"..nm.."}" end end function write_fndoc(h, fn, info) if not info.doc then return end fprintf(h, "\\begin{function}\n") if info.exported_name then fn=info.exported_name end --[[ if info.class~="global" then fprintf(h, "\\index{%s@%s!", texfriendly(info.class), texfriendly_typeormod(info.class)); fprintf(h, "%s@\\code{%s}}\n", texfriendly(fn), fn) end fprintf(h, "\\index{%s@\\code{%s}}\n", texfriendly(fn), fn) ]] if info.class~="global" then fprintf(h, "\\hyperlabel{fn:%s.%s}", info.class, fn) else fprintf(h, "\\hyperlabel{fn:%s}", fn) end fprintf(h, "\\synopsis{") if info.odesc then h:write(tohuman(info.odesc, info.otype).." ") end if info.class~="global" then fprintf(h, "%s.", info.class) end if not info.ivars then -- Lua input fprintf(h, "%s%s}", fn, info.paramstr) else fprintf(h, "%s(", fn) local comma="" for i, varname in pairs(info.ivars) do fprintf(h, comma .. "%s", tohuman(string.sub(info.idesc, i, i), info.itypes[i])) if varname then fprintf(h, " %s", varname) end comma=", " end fprintf(h, ")}\n") end h:write("\\begin{funcdesc}\n" .. trim(info.doc).. "\n\\end{funcdesc}\n") fprintf(h, "\\end{function}\n\n") end function write_class_documentation(h, cls, in_subsect) sorted={} if not classes[cls] or not classes[cls].fns then return end if in_subsect then fprintf(h, "\n\n\\subsection{\\type{%s} functions}\n\n", cls) end for fn in pairs(classes[cls].fns) do table.insert(sorted, fn) end table.sort(sorted) for _, fn in ipairs(sorted) do write_fndoc(h, fn, classes[cls].fns[fn]) end end function write_documentation(h) sorted={} write_class_documentation(h, module, false) for cls in pairs(classes) do if cls~=module then table.insert(sorted, cls) end end table.sort(sorted) for _, cls in ipairs(sorted) do write_class_documentation(h, cls, true) end end -- }}} -- main {{{ inputs={} outh=io.stdout header_file=nil output_file=nil make_docs=false module="global" i=1 function usage() print([[ Usage: libextl-mkexports [options] files... Where options include: -mkdoc -help -o outfile -h header -module module -reexport module ]]) os.exit() end while arg[i] do if arg[i]=="-help" then usage() elseif arg[i]=="-mkdoc" then make_docs=true elseif arg[i]=="-o" then i=i+1 output_file=arg[i] elseif arg[i]=="-h" then i=i+1 header_file=arg[i] elseif arg[i]=="-module" then i=i+1 module=arg[i] if not module then error("No module given") end elseif arg[i]=="-reexport" then i=i+1 reexports[arg[i]]=true else table.insert(inputs, arg[i]) end i=i+1 end if table.getn(inputs)==0 then usage() end for _, ifnam in pairs(inputs) do h, err=io.open(ifnam, "r") if not h then errorf("Could not open %s: %s", ifnam, err) end print("Scanning " .. ifnam .. " for exports.") data=h:read("*a") h:close() if string.find(ifnam, "%.lua$") then assert(make_docs) parse_luadoc("\n" .. data .. "\n") elseif string.find(ifnam, "%.c$") then parse("\n" .. data .. "\n") else error('Unknown file') end end if output_file then outh, err=io.open(output_file, "w") if not outh then error(err) end end if make_docs then write_documentation(outh) else write_exports(outh) if header_file then local hh, err=io.open(header_file, "w") if not hh then error(err) end write_header(hh) hh:close() end end -- }}}