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