]> git.decadent.org.uk Git - ion3.git/blob - libextl/libextl-mkexports.in
bbb672d84ae74961cdf4a8dd4ec11e234c424b86
[ion3.git] / libextl / libextl-mkexports.in
1 #!LUA50
2 -- -*- mode: lua -*-
3 -- ion/mkexports.lua
4 -- 
5 -- Copyright (c) Tuomo Valkonen 2003-2005.
6 -- 
7 -- Ion is free software; you can redistribute it and/or modify it under
8 -- the terms of the GNU Lesser General Public License as published by
9 -- the Free Software Foundation; either version 2.1 of the License, or
10 -- (at your option) any later version.
11 --
12 -- 
13 -- This is a script to automatically generate exported function registration
14 -- code and documentation for those from C source.
15 -- 
16 -- The script can also parse documentation comments from Lua code.
17 -- 
18
19 -- Helper functions {{{
20
21 function errorf(fmt, ...)
22     error(string.format(fmt, unpack(arg)), 2)
23 end
24
25 function matcherr(s)
26     error(string.format("Parse error in \"%s...\"", string.sub(s, 1, 50)), 2)
27 end
28
29 function fprintf(h, fmt, ...)
30     h:write(string.format(fmt, unpack(arg)))
31 end
32
33 function trim(str)
34     return string.gsub(str, "^[%s\n]*(.-)[%s\n]*$", "%1")
35 end
36
37 -- }}}
38
39
40 -- Some conversion tables {{{
41
42 desc2ct={
43     ["v"]="void",
44     ["i"]="int",
45     ["d"]="double",
46     ["b"]="bool",
47     ["t"]="ExtlTab",
48     ["f"]="ExtlFn",
49     ["o"]="Obj*",
50     ["s"]="char*",
51     ["S"]="const char*",
52 }
53
54 ct2desc={
55     ["uint"] = "i",
56 }
57
58 for d, t in pairs(desc2ct) do
59     ct2desc[t]=d
60 end
61
62 desc2human={
63     ["v"]="void",
64     ["i"]="integer",
65     ["d"]="double",
66     ["b"]="bool",
67     ["t"]="table",
68     ["f"]="function",
69     ["o"]="object",
70     ["s"]="string",
71     ["S"]="string",
72 }
73
74 -- }}}
75
76
77 -- Parser {{{
78
79 local classes={}
80 local chnds={}
81 local reexports={}
82
83 function add_chnd(fnt)
84     local odesc=string.gsub(fnt.odesc, "S", "s")
85     local idesc=string.gsub(fnt.idesc, "S", "s")
86     local str="l2chnd_" .. odesc .. "_" .. idesc .. "_"
87     
88     for i, t in ipairs(fnt.itypes) do
89         str=str .. "_" .. t
90     end
91     
92     chnds[str]={odesc=odesc, idesc=idesc, itypes=fnt.itypes}
93     fnt.chnd=str
94 end
95
96 function add_class(cls)
97     if cls~="Obj" and not classes[cls] then
98         classes[cls]={}
99     end
100 end
101
102 function sort_classes(cls)
103     local sorted={}
104     local inserted={}
105     
106     local function insert(cls)
107         if classes[cls] and not inserted[cls] then
108             if classes[cls].parent then
109                 insert(classes[cls].parent)
110             end
111             inserted[cls]=true
112             table.insert(sorted, cls)
113         end
114     end
115     
116     for cls in pairs(classes) do
117         insert(cls)
118     end
119     
120     return sorted
121 end
122
123 function parse_type(t)
124     local desc, otype, varname="?", "", ""
125     
126     -- Remove whitespace at start end end of the string and compress elsewhere.
127     t=string.gsub(trim(t), "[%s\n]+", " ")
128     -- Remove spaces around asterisks.
129     t=string.gsub(t, " *%* *", "*")
130     -- Add space after asterisks.
131     t=string.gsub(t, "%*", "* ")
132     
133     -- Check for const
134     local is_const=""
135     local s, e=string.find(t, "^const +")
136     if s then
137         is_const="const "
138         t=string.sub(t, e+1)
139     end
140     
141     -- Find variable name part
142     tn=t
143     s, e=string.find(tn, " ")
144     if s then
145         varname=string.sub(tn, e+1)
146         tn=string.sub(tn, 1, s-1)
147         assert(not string.find(varname, " "))
148     end
149     
150     -- Try to check for supported types
151     desc = ct2desc[is_const .. tn]
152     
153     if not desc or desc=="o" then
154         s, e=string.find(tn, "^[A-Z][%w_]*%*$")
155         if s then
156             desc="o"
157             otype=string.sub(tn, s, e-1)
158             add_class(otype)
159         else
160             errorf("Error parsing type from \"%s\"", t)
161         end
162     end
163     
164     return desc, otype, varname
165 end
166
167 function parse(d)
168     local doc=nil
169     local safe=false
170     local untraced=false
171     
172     -- Handle /*EXTL_DOC ... */
173     local function do_doc(s)
174         --s=string.gsub(s, "/%*EXTL_DOC(.-)%*/", "%1")
175         local st=string.len("/*EXTL_DOC")
176         local en, _=string.find(s, "%*/")
177         if not en then
178             errorf("Could not find end of comment in \"%s...\"",
179                    string.sub(s, 1, 50))
180         end
181         
182         s=string.sub(s, st+1, en-1)
183         s=string.gsub(s, "\n[%s]*%*", "\n")
184         doc=s
185     end
186
187     -- Handle EXTL_SAFE    
188     local function do_safe(s)
189         assert(not safe)
190         safe=true
191     end
192
193     -- Handle EXTL_UNTRACED
194     local function do_untraced(s)
195         assert(not untraced)
196         untraced=true
197     end
198     
199     local function do_do_export(cls, efn, ot, fn, param)
200         local odesc, otype=parse_type(ot)
201         local idesc, itypes, ivars="", {}, {}
202         
203         -- Parse arguments
204         param=string.sub(param, 2, -2)
205         if string.find(param, "[()]") then
206             errorf("Error: parameters to %s contain parantheses", fn)
207         end
208         param=trim(param)
209         if string.len(param)>0 then
210             for p in string.gfind(param .. ",", "([^,]*),") do
211                 local spec, objtype, varname=parse_type(p)
212                 idesc=idesc .. spec
213                 table.insert(itypes, objtype)
214                 table.insert(ivars, varname)
215             end
216         end
217         
218         if cls=="?" then
219             if string.sub(idesc, 1, 1)~="o" then
220                 error("Invalid class for " .. fn)
221             end
222             cls=itypes[1]
223         end
224         
225         -- Generate call handler name
226         
227         local fninfo={
228             doc=doc,
229             safe=safe,
230             untraced=untraced,
231             odesc=odesc,
232             otype=otype,
233             idesc=idesc,
234             itypes=itypes,
235             ivars=ivars,
236             exported_name=efn,
237             class=cls,
238         }
239         
240         add_chnd(fninfo)
241         add_class(cls)
242         
243         if not classes[cls].fns then
244             classes[cls].fns={}
245         end
246         
247         assert(not classes[cls].fns[fn], "Function " .. fn .. " multiply defined!")
248
249         classes[cls].fns[fn]=fninfo
250
251         -- Reset
252         doc=nil
253         safe=false
254         untraced=false
255     end
256
257     -- Handle EXTL_EXPORT otype fn(args)
258     local function do_export(s)
259         local mdl, efn
260         local pat="EXTL_EXPORT[%s\n]+([%w%s_*]+[%s\n*])([%w_]+)[%s\n]*(%b())"
261         local st, en, ot, fn, param=string.find(s, pat)
262         
263         if not st then matcherr(s) end
264         
265         if module=="global" or not module then
266             efn=fn
267             mdl=module
268         else
269             st, en, efn=string.find(fn, "^"..module.."_(.*)")
270             if efn then
271                 mdl=module
272             else
273                 for k in pairs(reexports) do
274                     st, en, efn=string.find(fn, "^"..k.."_(.*)")
275                     if efn then
276                         mdl=module
277                         break
278                     end
279                 end
280             end
281             
282             if not mdl then
283                 error('"'..fn..'" is not a valid function name of format '..
284                       'modulename_fnname.')
285             end
286         end
287         do_do_export(module, efn, ot, fn, param)
288     end
289
290     -- Handle EXTL_EXPORT_MEMBER otype prefix_fn(class, args)
291     local function do_export_member(s)
292         local pat="EXTL_EXPORT_MEMBER[%s\n]+([%w%s_*]+[%s\n*])([%w_]+)[%s\n]*(%b())"
293         local st, en, ot, fn, param=string.find(s, pat)
294         if not st then matcherr(s) end
295         local efn=string.gsub(fn, ".-_(.*)", "%1")
296         do_do_export("?", efn, ot, fn, param)
297     end
298
299     -- Handle EXTL_EXPORT_AS(table, member_fn) otype fn(args)
300     local function do_export_as(s)
301         local pat="EXTL_EXPORT_AS%(%s*([%w_]+)%s*,%s*([%w_]+)%s*%)[%s\n]+([%w%s_*]+[%s\n*])([%w_]+)[%s\n]*(%b())"
302         local st, en, cls, efn, ot, fn, param=string.find(s, pat)
303         if not st then matcherr(s) end
304         do_do_export((reexports[cls] and module or cls), efn, ot, fn, param)
305     end
306     
307     local function do_implobj(s)
308         local pat="IMPLCLASS%(%s*([%w_]+)%s*,%s*([%w_]+)%s*,[^)]*%)"
309         local st, en, cls, par=string.find(s, pat)
310         if not st then matcherr(s) end
311         add_class(cls)
312         classes[cls].parent=par
313     end
314
315     local function do_class(s)
316         local pat="EXTL_CLASS%(%s*([%w_]+)%s*,%s*([%w_]+)%s*%)"
317         local st, en, cls, par=string.find(s, pat)
318         if not st then matcherr(s) end
319         add_class(cls)
320         classes[cls].parent=par
321     end
322     
323     local lookfor={
324         {"/%*EXTL_DOC", do_doc},
325         {"[%s\n]EXTL_SAFE[%s\n]", do_safe},
326         {"[%s\n]EXTL_UNTRACED[%s\n]", do_untraced},
327         {"[%s\n]EXTL_EXPORT[%s\n]+IMPLCLASS", do_implobj},
328         {"[%s\n]EXTL_EXPORT[%s\n]", do_export},
329         {"[%s\n]EXTL_EXPORT_AS", do_export_as},
330         {"[%s\n]EXTL_EXPORT_MEMBER[%s\n]", do_export_member},
331         {"[%s\n]EXTL_CLASS", do_class},
332     }
333     
334     do_parse(d, lookfor)
335 end
336
337 function do_parse(d, lookfor)
338     while true do
339         local mins, mine, minfn=string.len(d)+1, nil, nil
340         for _, lf in ipairs(lookfor) do
341             local s, e=string.find(d, lf[1])
342             if s and s<mins then
343                 mins, mine, minfn=s, e, lf[2]
344             end
345         end
346         
347         if not minfn then
348             return
349         end
350         
351         minfn(string.sub(d, mins))
352         d=string.sub(d, mine)
353     end
354 end    
355
356 -- }}}
357
358
359 -- Parser for Lua code documentation {{{
360
361 function parse_luadoc(d)
362     function do_luadoc(s_)
363         local st, en, b, s=string.find(s_, "\n%-%-DOC(.-)(\n.*)")
364         if string.find(b, "[^%s]") then
365             errorf("Syntax error while parsing \"--DOC%s\"", b)
366         end
367         local doc, docl=""
368         while true do
369             st, en, docl=string.find(s, "^\n%s*%-%-([^\n]*\n)")
370             if not st then
371                 break
372             end
373             --print(docl)
374             doc=doc .. docl
375             s=string.sub(s, en)
376         end
377         
378         local fn, param
379         
380         st, en, fn, param=string.find(s, "^\n[%s\n]*function%s*([%w_:%.]+)%s*(%b())")
381
382         if not fn then
383             errorf("Syntax error while parsing \"%s\"",
384                    string.sub(s, 1, 50))
385         end
386         local cls, clsfn
387         st, en, cls, clsfn=string.find(fn, "^([^.]*)%.(.*)$")
388         
389         if cls and clsfn then
390             fn=clsfn
391         else
392             cls="global"
393         end
394         
395         fninfo={
396             doc=doc, 
397             paramstr=param,
398             class=cls,
399         }
400         
401         add_class(cls)
402         if not classes[cls].fns then
403             classes[cls].fns={}
404         end
405         classes[cls].fns[fn]=fninfo
406     end
407     
408     do_parse(d, {{"\n%-%-DOC", do_luadoc}})
409 end
410     
411 -- }}}
412
413
414 -- Export output {{{
415
416 function writechnd(h, name, info)
417     local oct=desc2ct[info.odesc]
418         
419     -- begin blockwrite
420     fprintf(h, [[
421 static bool %s(%s (*fn)(), ExtlL2Param *in, ExtlL2Param *out)
422 {
423 ]], name, oct)
424     -- end blockwrite
425
426     -- Generate type checking code
427     for k, t in pairs(info.itypes) do
428         if t~="" then
429             if k==1 then
430                 fprintf(h, "    if(!EXTL_CHKO1(in, %d, %s)) return FALSE;\n",
431                         k-1, t)
432             else
433                 fprintf(h, "    if(!EXTL_CHKO(in, %d, %s)) return FALSE;\n",
434                         k-1, t)
435             end
436         end
437     end
438
439     -- Generate function call code
440     if info.odesc=="v" then
441         fprintf(h, "    fn(")
442     else
443         fprintf(h, "    out[0].%s=fn(", info.odesc)
444     end
445     
446     comma=""
447     for k=1, string.len(info.idesc) do
448         fprintf(h, comma .. "in[%d].%s", k-1, string.sub(info.idesc, k, k))
449         comma=", "
450     end
451     fprintf(h, ");\n    return TRUE;\n}\n")
452 end    
453
454 function bool2str4c(b)
455     return (b and "TRUE" or "FALSE")
456 end        
457
458 function write_class_fns(h, cls, data)
459     fprintf(h, "\n\nstatic ExtlExportedFnSpec %s_exports[] = {\n", cls)
460     
461     for fn, info in pairs(data.fns) do
462         local ods, ids="NULL", "NULL"
463         if info.odesc~="v" then
464             ods='"' .. info.odesc .. '"'
465         end
466         
467         if info.idesc~="" then
468             ids='"' .. info.idesc .. '"'
469         end
470         
471         fprintf(h, "    {\"%s\", %s, %s, %s, (ExtlL2CallHandler*)%s, %s, %s, FALSE},\n",
472                 info.exported_name, fn, ids, ods, info.chnd, 
473                 bool2str4c(info.safe),
474                 bool2str4c(info.untraced))
475     end
476     
477     fprintf(h, "    {NULL, NULL, NULL, NULL, NULL, FALSE, FALSE, FALSE}\n};\n\n")
478 end
479
480
481 local function pfx(modname)
482     if modname=="global" or not modname then
483         return ""
484     else
485         return modname.."_"
486     end
487 end
488
489
490 function write_exports(h)
491     
492     -- begin blockwrite
493     h:write([[
494 /* Automatically generated by mkexports.lua */
495 #include <libextl/extl.h>
496 #include <libextl/private.h>
497
498 ]])
499     -- end blockwrite
500
501     -- Write class infos and check that the class is implemented in the 
502     -- module.
503     for c, data in pairs(classes) do
504         if string.lower(c)==c then
505             data.module=true
506         else
507             fprintf(h, "EXTL_DEFCLASS(%s);\n", c)
508             if data.fns and not data.parent then
509                 error(c..": Methods can only be registered if the class "
510                       .. "is implemented in the module in question.")
511             end
512         end
513     end
514     
515     -- Write L2 call handlers
516     for name, info in pairs(chnds) do
517         writechnd(h, name, info)
518     end
519     
520     fprintf(h, "\n")
521     
522     for cls, data in pairs(classes) do
523         if data.fns then
524             -- Write function declarations
525             for fn in pairs(data.fns) do
526                 fprintf(h, "extern void %s();\n", fn)
527             end
528             -- Write function table
529             write_class_fns(h, cls, data)
530         else
531             fprintf(h, "#define %s_exports NULL\n", cls)
532         end
533     end
534     
535     fprintf(h, "bool %sregister_exports()\n{\n", pfx(module))
536
537     local sorted_classes=sort_classes()
538     
539     for _, cls in pairs(sorted_classes) do
540         if cls=="global" then
541             fprintf(h, "    if(!extl_register_functions(global_exports)) return FALSE;\n")
542         elseif classes[cls].module then
543             fprintf(h, "    if(!extl_register_module(\"%s\", %s_exports)) return FALSE;\n", 
544                     cls, cls)
545         elseif classes[cls].parent then
546             fprintf(h, "    if(!extl_register_class(\"%s\", %s_exports, \"%s\")) return FALSE;\n",
547                     cls, cls, classes[cls].parent)
548         end
549     end
550
551     fprintf(h, "    return TRUE;\n}\n\nvoid %sunregister_exports()\n{\n", 
552             pfx(module))
553     
554     for _, cls in pairs(sorted_classes) do
555         if cls=="global" then
556             fprintf(h, "    extl_unregister_functions(global_exports);\n")
557         elseif classes[cls].module then
558             fprintf(h, "    extl_unregister_module(\"%s\", %s_exports);\n", 
559                     cls, cls)
560         elseif classes[cls].parent then
561             fprintf(h, "    extl_unregister_class(\"%s\", %s_exports);\n",
562                     cls, cls)
563         end
564     end
565     
566     fprintf(h, "}\n\n")
567 end
568
569
570 function write_header(h)
571     local p=pfx(module)
572     local u=string.upper(p)
573     fprintf(h, [[
574 /* Automatically generated by mkexports.lua */
575 #ifndef %sEXTL_EXPORTS_H
576 #define %sEXTL_EXPORTS_H
577
578 #include <libextl/extl.h>
579
580 extern bool %sregister_exports();
581 extern void %sunregister_exports();
582
583 #endif /* %sEXTL_EXPORTS_H */
584
585 ]], u, u, p, p, u)
586 end
587
588 -- }}}
589
590
591 -- Documentation output {{{
592
593 function tohuman(desc, objtype)
594     if objtype~="" then
595         return objtype
596     else
597         return desc2human[desc]
598     end
599 end
600
601 function texfriendly(name)
602     return string.gsub(name, "_", "-")
603 end
604
605 function texfriendly_typeormod(nm)
606     if string.find(nm, "A-Z") then
607         return "\\type{"..string.gsub(nm, '_', '\_').."}"
608     else
609         return "\\code{"..nm.."}"
610     end
611 end
612
613 function write_fndoc(h, fn, info)
614     if not info.doc then
615         return
616     end
617     fprintf(h, "\\begin{function}\n")
618     if info.exported_name then
619         fn=info.exported_name
620     end
621     
622     if info.class~="global" then
623         fprintf(h, "\\index{%s@%s!", texfriendly(info.class), 
624                 texfriendly_typeormod(info.class));
625         fprintf(h, "%s@\\code{%s}}\n", texfriendly(fn), fn)
626     end
627     fprintf(h, "\\index{%s@\\code{%s}}\n", texfriendly(fn), fn)
628     
629     if info.class~="global" then
630         fprintf(h, "\\hyperlabel{fn:%s.%s}", info.class, fn)
631     else
632         fprintf(h, "\\hyperlabel{fn:%s}", fn)
633     end
634     
635     fprintf(h, "\\synopsis{")
636     if info.odesc then
637         h:write(tohuman(info.odesc, info.otype).." ")
638     end
639     
640     if info.class~="global" then
641         fprintf(h, "%s.", info.class)
642     end
643     
644     if not info.ivars then
645         -- Lua input
646         fprintf(h, "%s%s}", fn, info.paramstr)
647     else
648         fprintf(h, "%s(", fn)
649         local comma=""
650         for i, varname in pairs(info.ivars) do
651             fprintf(h, comma .. "%s", tohuman(string.sub(info.idesc, i, i),
652                                               info.itypes[i]))
653             if varname then
654                 fprintf(h, " %s", varname)
655             end
656             comma=", "
657         end
658         fprintf(h, ")}\n")
659     end
660     h:write("\\begin{funcdesc}\n" .. trim(info.doc).. "\n\\end{funcdesc}\n")
661     fprintf(h, "\\end{function}\n\n")
662 end
663
664
665 function write_class_documentation(h, cls, in_subsect)
666     sorted={}
667     
668     if not classes[cls] or not classes[cls].fns then
669         return
670     end
671     
672     if in_subsect then
673         fprintf(h, "\n\n\\subsection{\\type{%s} functions}\n\n", cls)
674     end
675
676     for fn in pairs(classes[cls].fns) do
677         table.insert(sorted, fn)
678     end
679     table.sort(sorted)
680     
681     for _, fn in ipairs(sorted) do
682         write_fndoc(h, fn, classes[cls].fns[fn])
683     end
684 end
685
686
687 function write_documentation(h)
688     sorted={}
689     
690     write_class_documentation(h, module, false)
691     
692     for cls in pairs(classes) do
693         if cls~=module then
694             table.insert(sorted, cls)
695         end
696     end
697     table.sort(sorted)
698     
699     for _, cls in ipairs(sorted) do
700         write_class_documentation(h, cls, true)
701     end
702 end
703
704 -- }}}
705
706
707 -- main {{{
708
709 inputs={}
710 outh=io.stdout
711 header_file=nil
712 output_file=nil
713 make_docs=false
714 module="global"
715 i=1
716
717 function usage()
718     print([[
719 Usage: libextl-mkexports [options] files...
720
721 Where options include:
722     -mkdoc
723     -help
724     -o outfile
725     -h header
726     -module module
727     -reexport module
728 ]])
729     os.exit()
730 end
731
732 while arg[i] do
733     if arg[i]=="-help" then
734         usage()
735     elseif arg[i]=="-mkdoc" then
736         make_docs=true
737     elseif arg[i]=="-o" then
738         i=i+1
739         output_file=arg[i]
740     elseif arg[i]=="-h" then
741         i=i+1
742         header_file=arg[i]
743     elseif arg[i]=="-module" then
744         i=i+1
745         module=arg[i]
746         if not module then
747             error("No module given")
748         end
749     elseif arg[i]=="-reexport" then
750         i=i+1
751         reexports[arg[i]]=true
752     else
753         table.insert(inputs, arg[i])
754     end
755     i=i+1
756 end
757
758 if table.getn(inputs)==0 then
759     usage()
760 end
761
762 for _, ifnam in pairs(inputs) do
763     h, err=io.open(ifnam, "r")
764     if not h then
765             errorf("Could not open %s: %s", ifnam, err)
766     end
767     print("Scanning " .. ifnam .. " for exports.")
768     data=h:read("*a")
769     h:close()
770     if string.find(ifnam, "%.lua$") then
771         assert(make_docs)
772         parse_luadoc("\n" .. data .. "\n")
773     elseif string.find(ifnam, "%.c$") then
774         parse("\n" .. data .. "\n")
775     else
776         error('Unknown file')
777     end
778     
779 end
780
781 if output_file then        
782     outh, err=io.open(output_file, "w")
783     if not outh then
784         error(err)
785     end
786 end
787
788 if make_docs then
789     write_documentation(outh)
790 else
791     write_exports(outh)
792     if header_file then
793         local hh, err=io.open(header_file, "w")
794         if not hh then
795             error(err)
796         end
797         write_header(hh)
798         hh:close()
799     end
800 end
801
802 -- }}}
803