1 /*
2  * This file is part of gtkD.
3  *
4  * gtkD is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU Lesser General Public License
6  * as published by the Free Software Foundation; either version 3
7  * of the License, or (at your option) any later version, with
8  * some exceptions, please read the COPYING file.
9  *
10  * gtkD is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU Lesser General Public License for more details.
14  *
15  * You should have received a copy of the GNU Lesser General Public License
16  * along with gtkD; if not, write to the Free Software
17  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110, USA
18  */
19 
20 module utils.GtkWrapper;
21 
22 import std.algorithm;
23 import std.array;
24 import std.file;
25 import std.uni;
26 import std.path;
27 import std.stdio;
28 
29 import utils.DefReader;
30 import utils.IndentedStringBuilder;
31 import utils.GtkPackage;
32 import utils.GtkStruct;
33 import utils.GtkFunction;
34 import utils.GtkType;
35 import utils.WrapError;
36 
37 void main(string[] args)
38 {
39 	GtkWrapper wrapper = new GtkWrapper("./");
40 	wrapper.proccess("APILookup.txt");
41 
42 	if ( args.length > 1 && args[1].among("-f", "--print-free") )
43 		wrapper.printFreeFunctions();
44 
45 	foreach(pack; GtkWrapper.packages)
46 	{
47 		if ( pack.name == "cairo" )
48 			continue;
49 
50 		pack.writeTypes();
51 		pack.writeLoaderTable();
52 		pack.writeClasses();
53 	}
54 }
55 
56 class GtkWrapper
57 {
58 	bool includeComments;
59 
60 	string apiRoot;
61 	string inputRoot;
62 	string outputRoot;
63 	string srcDir;
64 	string bindDir;
65 
66 	static string licence;
67 	static string[string] aliasses;
68 
69 	static GtkPackage[string] packages;
70 
71 	public this(string apiRoot)
72 	{
73 		this.apiRoot = apiRoot;
74 	}
75 
76 	public void proccess(string apiLookupDefinition)
77 	{
78 		DefReader defReader = new DefReader( buildPath(apiRoot, apiLookupDefinition) );
79 
80 		while ( !defReader.empty )
81 		{
82 			switch ( defReader.key )
83 			{
84 				case "license":
85 					licence = defReader.readBlock().join();
86 					break;
87 				case "includeComments":
88 					includeComments = defReader.valueBool;
89 					break;
90 				case "alias":
91 					loadAA(aliasses, defReader);
92 					break;
93 				case "inputRoot":
94 					inputRoot = defReader.value;
95 					break;
96 				case "outputRoot":
97 					outputRoot = defReader.value;
98 					break;
99 				case "srcDir":
100 					srcDir = defReader.value;
101 					break;
102 				case "bindDir":
103 					bindDir = defReader.value;
104 					break;
105 				case "copy":
106 					if ( srcDir.empty )
107 						throw new WrapError(defReader, "Can't copy the file when srcDir is not set");
108 
109 					string outDir = buildPath(outputRoot, srcDir);
110 
111 					if ( !exists(outDir) )
112 					{
113 						try
114 							mkdirRecurse(outDir);
115 						catch (FileException)
116 							throw new WrapError(defReader, "Failed to create directory: "~ outDir);
117 					}
118 
119 					copyFile(apiRoot, buildPath(outputRoot, srcDir), defReader.value);
120 					break;
121 				case "lookup":
122 					proccess(defReader.value);
123 					break;
124 				case "wrap":
125 					if ( inputRoot.empty )
126 						throw new WrapError(defReader, "Found wrap while inputRoot isn't set");
127 					if ( outputRoot.empty )
128 						throw new WrapError(defReader, "Found wrap while outputRoot isn't set");
129 					if ( srcDir.empty )
130 						throw new WrapError(defReader, "Found wrap while srcDir isn't set");
131 					if ( bindDir.empty )
132 						throw new WrapError(defReader, "Found wrap while bindDir isn't set");
133 
134 					wrapPackage(defReader);
135 					break;
136 				default:
137 					throw new WrapError(defReader, "Unknown key: "~defReader.key);
138 			}
139 
140 			defReader.popFront();
141 		}
142 	}
143 
144 	public void wrapPackage(DefReader defReader)
145 	{
146 		GtkPackage pack;
147 		GtkStruct currentStruct;
148 
149 		try
150 		{
151 			if (defReader.value in packages)
152 				throw new WrapError(defReader, "Package: "~ defReader.value ~"already defined.");
153 
154 			pack = new GtkPackage(defReader.value, this, srcDir, bindDir);
155 			packages[defReader.value] = pack;
156 			defReader.popFront();
157 		}
158 		catch (Exception e)
159 			throw new WrapError(defReader, e.msg);
160 
161 		while ( !defReader.empty )
162 		{
163 			switch ( defReader.key )
164 			{
165 				case "addAliases":
166 					pack.lookupAliases ~= defReader.readBlock();
167 					break;
168 				case "addEnums":
169 					pack.lookupEnums ~= defReader.readBlock();
170 					break;
171 				case "addStructs":
172 					pack.lookupStructs ~= defReader.readBlock();
173 					break;
174 				case "addFuncts":
175 					pack.lookupFuncts ~= defReader.readBlock();
176 					break;
177 				case "addConstants":
178 					pack.lookupConstants ~= defReader.readBlock();
179 					break;
180 				case "file":
181 					pack.parseGIR(defReader.value);
182 					break;
183 				case "struct":
184 					if ( defReader.value.empty )
185 					{
186 						currentStruct = null;
187 					}
188 					else
189 					{
190 						currentStruct = pack.getStruct(defReader.value);
191 						if ( currentStruct is null )
192 							currentStruct = createClass(pack, defReader.value);
193 					}
194 					break;
195 				case "class":
196 					if ( currentStruct is null )
197 						currentStruct = createClass(pack, defReader.value);
198 
199 					currentStruct.lookupClass = true;
200 					currentStruct.name = defReader.value;
201 					break;
202 				case "interface":
203 					if ( currentStruct is null )
204 						currentStruct = createClass(pack, defReader.value);
205 
206 					currentStruct.lookupInterface = true;
207 					currentStruct.name = defReader.value;
208 					break;
209 				case "cType":
210 					currentStruct.cType = defReader.value;
211 					break;
212 				case "namespace":
213 					currentStruct.type = GtkStructType.Record;
214 					currentStruct.lookupClass = false;
215 					currentStruct.lookupInterface = false;
216 
217 					if ( defReader.value.empty )
218 					{
219 						currentStruct.noNamespace = true;
220 					}
221 					else
222 					{
223 						currentStruct.noNamespace = false;
224 						currentStruct.name = defReader.value;
225 					}
226 					break;
227 				case "extend":
228 					currentStruct.lookupParent = true;
229 					currentStruct.parent = defReader.value;
230 					break;
231 				case "implements":
232 					if ( defReader.value.empty )
233 						currentStruct.implements = null;
234 					else
235 						currentStruct.implements ~= defReader.value;
236 					break;
237 				case "merge":
238 					GtkStruct mergeStruct = pack.getStruct(defReader.value);
239 					currentStruct.merge(mergeStruct);
240 					GtkStruct copy = currentStruct.dup();
241 					copy.noCode = true;
242 					copy.noExternal = true;
243 					mergeStruct.pack.collectedStructs[mergeStruct.name] = copy;
244 					break;
245 				case "move":
246 					string[] vals = defReader.value.split();
247 					if ( vals.length <= 1 )
248 						throw new WrapError(defReader, "No destination for move: "~ defReader.value);
249 					string newFuncName = ( vals.length == 3 ) ? vals[2] : vals[0];
250 					GtkStruct dest = pack.getStruct(vals[1]);
251 					if ( dest is null )
252 						dest = createClass(pack, vals[1]);
253 					if ( vals[0] !in pack.collectedFunctions )
254 						throw new WrapError(defReader, "unknown function "~ vals[0]);
255 					pack.collectedFunctions[vals[0]].strct = dest;
256 					dest.functions[newFuncName] = pack.collectedFunctions[vals[0]];
257 					dest.functions[newFuncName].name = newFuncName;
258 					pack.collectedFunctions.remove(vals[0]);
259 					break;
260 				case "import":
261 					currentStruct.imports ~= defReader.value;
262 					break;
263 				case "structWrap":
264 					loadAA(currentStruct.structWrap, defReader);
265 					break;
266 				case "alias":
267 					loadAA(currentStruct.aliases, defReader);
268 					break;
269 				case "override":
270 					currentStruct.functions[defReader.value].lookupOverride = true;
271 					break;
272 				case "noAlias":
273 					pack.collectedAliases.remove(defReader.value);
274 					break;
275 				case "noEnum":
276 					pack.collectedEnums.remove(defReader.value);
277 					break;
278 				case "noCallback":
279 					pack.collectedCallbacks.remove(defReader.value);
280 					break;
281 				case "noCode":
282 					if ( defReader.valueBool )
283 					{
284 						currentStruct.noCode = true;
285 						break;
286 					}
287 					if ( defReader.value !in currentStruct.functions )
288 						throw new WrapError(defReader, "Unknown function: "~ defReader.value);
289 
290 					currentStruct.functions[defReader.value].noCode = true;
291 					break;
292 				case "noExternal":
293 					currentStruct.noExternal = true;
294 					break;
295 				case "noSignal":
296 					currentStruct.functions[defReader.value~"-signal"].noCode = true;
297 					break;
298 				case "noStruct":
299 					currentStruct.noDecleration = true;
300 					break;
301 				case "code":
302 					currentStruct.lookupCode ~= defReader.readBlock;
303 					break;
304 				case "interfaceCode":
305 					currentStruct.lookupInterfaceCode ~= defReader.readBlock;
306 					break;
307 				case "in":
308 					string[] vals = defReader.value.split();
309 					if ( vals[0] !in currentStruct.functions )
310 						throw new WrapError(defReader, "Unknown function: "~ vals[0]);
311 					findParam(currentStruct, vals[0], vals[1]).direction = GtkParamDirection.Default;
312 					break;
313 				case "out":
314 					string[] vals = defReader.value.split();
315 					if ( vals[0] !in currentStruct.functions )
316 						throw new WrapError(defReader, "Unknown function: "~ vals[0]);
317 					findParam(currentStruct, vals[0], vals[1]).direction = GtkParamDirection.Out;
318 					break;
319 				case "inout":
320 				case "ref":
321 					string[] vals = defReader.value.split();
322 					if ( vals[0] !in currentStruct.functions )
323 						throw new WrapError(defReader, "Unknown function: "~ vals[0]);
324 					findParam(currentStruct, vals[0], vals[1]).direction = GtkParamDirection.InOut;
325 					break;
326 				case "array":
327 					string[] vals = defReader.value.split();
328 
329 					if ( vals[0] !in currentStruct.functions )
330 						throw new WrapError(defReader, "Unknown function: "~ vals[0]);
331 
332 					GtkFunction func = currentStruct.functions[vals[0]];
333 
334 					if ( vals[1] == "Return" )
335 					{
336 						if ( vals.length < 3 )
337 						{
338 							func.returnType.zeroTerminated = true;
339 							break;
340 						}
341 
342 						GtkType elementType = new GtkType(this);
343 
344 						elementType.name = func.returnType.name;
345 						elementType.cType = func.returnType.cType[0..$-1];
346 						func.returnType.elementType = elementType;
347 
348 						foreach( i, p; func.params )
349 						{
350 							if ( p.name == vals[2] )
351 								func.returnType.length = cast(int)i;
352 						}
353 					}
354 					else
355 					{
356 						GtkParam param = findParam(currentStruct, vals[0], vals[1]);
357 						GtkType elementType = new GtkType(this);
358 
359 						elementType.name = param.type.name;
360 						elementType.cType = param.type.cType[0..$-1];
361 						param.type.elementType = elementType;
362 
363 						if ( vals.length < 3 )
364 						{
365 							param.type.zeroTerminated = true;
366 							break;
367 						}
368 
369 						if ( vals[2] == "Return" )
370 						{
371 							param.type.length = -2;
372 							break;
373 						}
374 
375 						foreach( i, p; func.params )
376 						{
377 							if ( p.name == vals[2] )
378 								param.type.length = cast(int)i;
379 						}
380 					}
381 					break;
382 				case "copy":
383 					if ( srcDir.empty )
384 						throw new WrapError(defReader,
385 						                    "Can't copy the file when srcDir is not set");
386 
387 					copyFile(apiRoot, buildPath(outputRoot, srcDir), defReader.value);
388 					break;
389 				default:
390 					throw new WrapError(defReader, "Unknown key: "~defReader.key);
391 			}
392 
393 			defReader.popFront();
394 		}
395 	}
396 
397 	void printFreeFunctions()
398 	{
399 		foreach ( pack; packages )
400 		{
401 			foreach ( func; pack.collectedFunctions )
402 			{
403 				if ( func.movedTo.empty )
404 					writefln("%s: %s", pack.name, func.name);
405 			}
406 		}
407 	}
408 
409 	private GtkParam findParam(GtkStruct strct, string func, string name)
410 	{
411 		foreach( param; strct.functions[func].params )
412 		{
413 			if ( param.name == name )
414 				return param;
415 		}
416 
417 		return null;
418 	}
419 
420 	private void loadAA (ref string[string] aa, DefReader defReader)
421 	{
422 		string[] vals = defReader.value.split();
423 
424 		if ( vals.length == 1 )
425 			vals ~= "";
426 
427 		if ( vals.length == 2 )
428 			aa[vals[0]] = vals[1];
429 		else
430 			throw new WrapError(defReader, "Unknown key: "~defReader.key);
431 	}
432 
433 	private void copyFile(string srcDir, string destDir, string file)
434 	{
435 		string from = buildPath(srcDir, file);
436 		string to   = buildPath(destDir, file);
437 
438 		writefln("copying file [%s] to [%s]", from, to);
439 		copy(from, to);
440 
441 		if ( !exists(to) )
442 			throw new Exception("Cannot copy  file: "~from);
443 	}
444 
445 	private GtkStruct createClass(GtkPackage pack, string name)
446 	{
447 		GtkStruct strct = new GtkStruct(this, pack);
448 		strct.name = name;
449 		strct.cType = pack.cTypePrefix ~ name;
450 		strct.type = GtkStructType.Record;
451 		strct.noDecleration = true;
452 		pack.collectedStructs["lookup"~name] = strct;
453 
454 		return strct;
455 	}
456 }
457 
458 /**
459  * Apply aliasses to the tokens in the string, and
460  * camelCase underscore separated tokens.
461  */
462 string stringToGtkD(string str, string[string] aliases, string[string] localAliases = null)
463 {
464 	size_t pos, start;
465 	string seps = " \n\r\t\f\v()[]*,;";
466 	auto converted = appender!string();
467 
468 	while ( pos < str.length )
469 	{
470 		if ( !seps.canFind(str[pos]) )
471 		{
472 			start = pos;
473 
474 			while ( pos < str.length && !seps.canFind(str[pos]) )
475 				pos++;
476 
477 			//Workaround for the tm struct, type and variable have the same name.
478 			if ( pos < str.length && str[pos] == '*' && str[start..pos] == "tm" )
479 				converted.put("void");
480 			else
481 				converted.put(tokenToGtkD(str[start..pos], aliases, localAliases));
482 
483 			if ( pos == str.length )
484 				break;
485 		}
486 
487 		converted.put(str[pos]);
488 		pos++;
489 	}
490 
491 	return converted.data;
492 }
493 
494 unittest
495 {
496 	assert(stringToGtkD("token", ["token":"tok"]) == "tok");
497 	assert(stringToGtkD("string token_to_gtkD(string token, string[string] aliases)", ["token":"tok"])
498 	       == "string tokenToGtkD(string tok, string[string] aliases)");
499 }
500 
501 string tokenToGtkD(string token, string[string] aliases, bool caseConvert=true)
502 {
503 	return tokenToGtkD(token, aliases, null, caseConvert);
504 }
505 
506 string tokenToGtkD(string token, string[string] aliases, string[string] localAliases, bool caseConvert=true)
507 {
508 	if ( token in localAliases )
509 		return localAliases[token];
510 	else if ( token in aliases )
511 		return aliases[token];
512 	else if ( token.startsWith("cairo_") && token.endsWith("_t", "_t*", "_t**") )
513 		return token;
514 	else if ( token == "pid_t" )
515 		return token;
516 	else if ( caseConvert )
517 		return tokenToGtkD(removeUnderscore(token), aliases, localAliases, false);
518 	else
519 		return token;
520 }
521 
522 string removeUnderscore(string token)
523 {
524 	char pc;
525 	auto converted = appender!string();
526 
527 	while ( !token.empty )
528 	{
529 		if ( token[0] == '_' )
530 		{
531 			pc = token[0];
532 			token = token[1..$];
533 
534 			continue;
535 		}
536 
537 		if ( pc == '_' )
538 			converted.put(token[0].toUpper());
539 		else
540 			converted.put(token[0]);
541 
542 		pc = token[0];
543 		token = token[1..$];
544 	}
545 
546 	return converted.data;
547 }
548 
549 unittest
550 {
551 	assert(removeUnderscore("this_is_a_test") == "thisIsATest");
552 }