yum-mirror/slang
Making it easier to work with shaders
git clone https://git.yummers.dev/yum-mirror/slang
b9300bae0
master
1// slang-embed.cpp 2 3// This file implements a simple utility for taking an input file 4// and embedding into a C++ source file as a `static const` array. 5 6// For now this utility uses plain C stdlib functionality rather 7// than depending on any of the utiltiies from the Slang project 8// libraries. 9// 10#ifdef _MSC_VER 11#pragma warning(disable : 4996) 12#endif 13#include "../../source/core/slang-dictionary.h" 14#include "../../source/core/slang-io.h" 15#include "../../source/core/slang-list.h" 16#include "../../source/core/slang-string-util.h" 17#include "../../source/core/slang-string.h" 18 19#include <stdio.h> 20#include <stdlib.h> 21#include <string.h> 22 23// Utility to free pointers on scope exit 24struct ScopedMemory 25{ 26ScopedMemory (void * ptr ) 27 :ptr (ptr ) 28 { 29 } 30 31 ~ScopedMemory () 32 { 33if (ptr ) 34free (ptr ); 35 } 36 37void * ptr ; 38}; 39 40// Utility to close file on scope exit 41struct ScopedFile 42{ 43ScopedFile (FILE * file ) 44 :file (file ) 45 { 46 } 47 48 ~ScopedFile () 49 { 50if (file ) 51fclose (file ); 52 } 53 54FILE * file ; 55}; 56 57// The utility is implemented as a single `struct` type 58// that provides a context for the code. We do this as 59// an alternative to using global variables for passing 60// around options easily. 61// 62struct App 63{ 64char const * appName = "slang-embed" ; 65char const * inputPath = nullptr ; 66char const * outputPath = nullptr ; 67Slang ::List < Slang ::String > includeDirs ; 68Slang ::HashSet < Slang ::String > includedFiles ; 69size_t charCount = 0 ; 70bool useNewStringLit = true; 71 72void parseOptions (int argc ,char ** argv ) 73 { 74// First, get the program name 75if (argc > 0 ) 76 { 77appName = * argv ++ ; 78argc -- ; 79 } 80 81// Parse remaining arguments - we need at least inputPath 82if (argc < 1 ) 83 { 84fprintf (stderr ,"usage: %s inputPath [outputPath] [-I<includeDir> ...]\n" ,appName ); 85exit (1 ); 86 } 87 88// Get input path (first positional argument) 89inputPath = * argv ++ ; 90argc -- ; 91 92// Process remaining arguments 93while (argc > 0 ) 94 { 95char * arg = * argv ++ ; 96argc -- ; 97 98// Check for -I prefix for include directories 99if (strncmp (arg ,"-I" ,2 )== 0 ) 100 { 101// Check if this is a concatenated string of include directories 102char * startPtr = arg ; 103 104// Process the entire string as potentially multiple -I directives 105while (startPtr && * startPtr ) 106 { 107// Find the -I prefix 108char * iPos = strstr (startPtr ,"-I" ); 109if (!iPos ) 110break ; 111 112// Move past the -I 113char * dirStart = iPos + 2 ; 114 115// Find the next -I or end of string 116char * nextIPos = strstr (dirStart ,"-I" ); 117 118// Determine end of current include dir 119char * dirEnd = nextIPos ?nextIPos : (startPtr + strlen (startPtr )); 120 121// Check if the directory has a semicolon or quotes at the end 122if (dirEnd > dirStart && (* (dirEnd - 1 )== ';' || * (dirEnd - 1 )== '"' )) 123dirEnd -- ; 124 125// Save the current directory by creating a substring 126if (dirEnd > dirStart ) 127 { 128// Create a null-terminated copy 129size_t dirLen = dirEnd - dirStart ; 130Slang ::String tempDir (Slang ::UnownedStringSlice (dirStart ,dirLen )); 131 132// Remove any quotes 133if (tempDir [0 ]== '"' ) 134tempDir = tempDir .subString (1 ,tempDir .getLength ()- 1 ); 135if (tempDir .endsWith ("\"" )&& tempDir .getLength ()> 0 ) 136tempDir = tempDir .subString (0 ,tempDir .getLength ()- 1 ); 137 138// Remove trailing whitespace 139tempDir = tempDir .trimEnd (); 140 141// Add to include dirs 142includeDirs .add (tempDir ); 143 } 144 145// Move to next position (if any) 146startPtr = nextIPos ; 147 } 148 } 149// Otherwise treat as output path if not already set 150else if (!outputPath ) 151 { 152outputPath = arg ; 153 } 154else 155 { 156fprintf (stderr ,"unexpected argument: %s\n" ,arg ); 157fprintf (stderr ,"usage: %s inputPath [outputPath] [-I<includeDir> ...]\n" ,appName ); 158exit (1 ); 159 } 160 } 161 162// Validate we have the required arguments 163if (!inputPath ) 164 { 165fprintf (stderr ,"usage: %s inputPath [outputPath] [-I<includeDir> ...]\n" ,appName ); 166exit (1 ); 167 } 168 } 169 170void processInputFile (FILE * outputFile ,Slang ::String inputPath ) 171 { 172using namespace Slang ; 173 174String canonicalPath ; 175if (SLANG_SUCCEEDED (Slang ::Path ::getCanonical (inputPath ,canonicalPath ))) 176 { 177if (!includedFiles .add (canonicalPath )) 178return ; 179 } 180 181// We open the input file in text mode because we are currently 182// embedding textual source files. If/when this utility gets 183// used for binary files another mode could be called for. 184// 185// (Alternatively, we might always use binary mode, but this 186// could lead to a difference in the embedded bytes based on 187// the line ending convention of the host platform) 188// 189 190String contents ; 191 { 192auto res = File ::readAllText (inputPath ,contents ); 193SLANG_ASSERT (SLANG_SUCCEEDED (res )); 194 } 195 196LineParser lineReader (contents .getUnownedSlice ()); 197 198for (auto line :lineReader ) 199 { 200auto trimedLine = line .trimStart (); 201if (trimedLine .startsWith ("#include" )) 202 { 203auto fileName = Slang ::StringUtil ::getAtInSplit (trimedLine ,' ' ,1 ); 204bool isSystemInclude = false; 205 206// Handle both quoted and angle-bracket includes 207if (fileName [0 ]== '<' ) 208 { 209// Handle <filename> format 210isSystemInclude = true; 211// Extract filename between < and > 212if (fileName .getLength () >=2 && fileName [fileName .getLength ()- 1 ]== '>' ) 213 { 214fileName = 215Slang ::UnownedStringSlice (fileName .begin ()+ 1 ,fileName .end ()- 1 ); 216 } 217else 218 { 219 gotonormalProcess ;// Malformed include, skip it 220 } 221 } 222else if (fileName [0 ]== '"' && fileName [fileName .getLength ()- 1 ]== '"' ) 223 { 224// Handle "filename" format 225fileName = Slang ::UnownedStringSlice (fileName .begin ()+ 1 ,fileName .end ()- 1 ); 226 } 227else 228 { 229// Malformed include, skip it 230 gotonormalProcess ; 231 } 232 233// For system includes, only look in include dirs, not relative to current file 234auto path = isSystemInclude ?Slang ::String () 235 :Slang ::Path ::combine ( 236Slang ::Path ::getParentDirectory (inputPath ), 237fileName ); 238 239bool foundInclude = false; 240if (isSystemInclude || !Slang ::File ::exists (path )) 241 { 242// Try looking in each of the include directories 243for (auto & includeDir :includeDirs ) 244 { 245path = Slang ::Path ::combine (includeDir ,fileName ); 246if (Slang ::File ::exists (path )) 247 { 248foundInclude = true; 249break ; 250 } 251 } 252 } 253else 254 { 255foundInclude = true; 256 } 257 258if (!foundInclude ) 259 gotonormalProcess ; 260processInputFile (outputFile ,path .getUnownedSlice ()); 261continue ; 262 } 263normalProcess :; 264if (!useNewStringLit && charCount + line .getLength ()> 0x4000 ) 265 { 266charCount = 0 ; 267useNewStringLit = true; 268fprintf (outputFile ,";\n" ); 269 } 270if (useNewStringLit ) 271 { 272fprintf (outputFile ,"sb << \n\"" ); 273useNewStringLit = false; 274 } 275else 276 { 277fprintf (outputFile ,"\"" ); 278 } 279charCount += line .getLength (); 280for (auto c :line ) 281 { 282// Based on the byte that we are trying to emit, 283// we may need to emit an escape sequence. 284// 285switch (c ) 286 { 287// The common C escape sequencs are handled directly. 288// 289case '"' : 290fprintf (outputFile ,"\\\"" ); 291break ; 292case '\n' : 293fprintf (outputFile ,"\\n" ); 294break ; 295case '\t' : 296fprintf (outputFile ,"\\t" ); 297break ; 298case '\\' : 299fprintf (outputFile ,"\\\\" ); 300break ; 301default : 302// For all other cases, we detect if the byte 303// is in the printable ASCII range, and emit 304// it directly if sco. 305// 306if (c >=32 && c <=126 ) 307 { 308fputc (c ,outputFile ); 309 } 310else 311 { 312// Otherwise, we emit the byte as an octal 313// escape sequence, being sure to emit a 314// full three digits to avoid errorneous 315// encoding if the following byte might 316// represent a digit. 317// 318fprintf (outputFile ,"\\%03o" ,c ); 319 } 320break ; 321 } 322 } 323fprintf (outputFile ,"\\n\"\n" ); 324 } 325 } 326 327void processInputFile () 328 { 329// Note: Eventually we might support multiple input files in a 330// single invocation of the tool, but for now we only have 331// a single file to process. 332 333// We derive an output path simply by appending `.cpp` to the input 334// path, if not otherwise specified 335char * defaultOutputPath = (char * )malloc (strlen (inputPath )+ strlen (".cpp" )+ 1 ); 336ScopedMemory outputPathCleanup (defaultOutputPath ); 337strcpy (defaultOutputPath ,inputPath ); 338strcat (defaultOutputPath ,".cpp" ); 339if (!outputPath ) 340outputPath = defaultOutputPath ; 341 342FILE * outputFile = fopen (outputPath ,"w" ); 343ScopedFile outputFileCleanup (outputFile ); 344if (!outputFile ) 345 { 346fprintf (stderr ,"%s: error: failed to open '%s' for reading\n" ,appName ,outputPath ); 347exit (1 ); 348 } 349 350// We want to derive a variable name based on the name of 351// the input file we are mbedded. Toward this end, we 352// start by trying to strip off any leading directories 353// in the path. This logic is ad hoc but should suffice, 354// given that we don't plan to give the files we embed 355// unconventional names. 356// 357char const * fileName = inputPath ; 358if (auto pos = strrchr (fileName ,'\\' )) 359fileName = pos + 1 ; 360if (auto pos = strrchr (fileName ,'/' )) 361fileName = pos + 1 ; 362 363// The variable name will start as a copy of the file 364// name, although we will immediately drop any extension 365// that comes after a `.` to trim the name further. 366// 367char * variableName = (char * )malloc (strlen (fileName )+ 1 ); 368ScopedMemory variableNameCleanup (variableName ); 369strcpy (variableName ,fileName ); 370if (auto pos = strchr (variableName ,'.' )) 371* pos = 0 ; 372 373// We will also replace any `-` in the file name with 374// a `_` in the generate variable name, so that the 375// tool will be compatible with our current naming 376// convention of using `-` as the separator in file names. 377// 378for (auto cursor = variableName ;* cursor ;++ cursor ) 379 { 380switch (* cursor ) 381 { 382default : 383break ; 384case '-' : 385* cursor = '_' ; 386 } 387 } 388 389// With all the preliminaries out of the way, the actual 390// task of outputting the generated source file is simple. 391// 392fprintf (outputFile ,"// generated code; do not edit\n" ); 393fprintf (outputFile ,"#include \"../source/core/slang-basic.h\"\n" ); 394 395fprintf (outputFile ,"Slang::String get_%s()\n" ,variableName ); 396fprintf (outputFile ,"{\n" ); 397fprintf (outputFile ,"Slang::StringBuilder sb;\n" ); 398 399// Note: For now we are embedding the file as a string 400// literal, with full knowledge that this strategy 401// will run into limitations in certain compilers 402// (e.g., some versions of the Visual C++ compiler 403// don't handle string literals larger than 64KB). 404// 405// TODO: Eventually we should replace this logic with 406// code to emit a plain array of `unsigned char` with 407// an array initializer list `{ ... }`. While some 408// compilers have limitations or performance issues 409// with large array literals, the practical limits 410// appear to be higher than they are for string literals. 411 412processInputFile (outputFile ,Slang ::UnownedStringSlice (inputPath )); 413 414fprintf (outputFile ,";\n" ); 415fprintf (outputFile ,"return sb.produceString();\n}\n" ); 416 } 417}; 418 419int main (int argc ,char ** argv ) 420{ 421App app ; 422app .parseOptions (argc ,argv ); 423app .processInputFile (); 424return 0 ; 425}