使用 Scintilla 和 CINT 构建一个简单的 C++ 脚本编译器






4.74/5 (25投票s)
如何从 Scintilla 和 CINT 构建一个简单的 C++ 脚本编译器。
引言
我最近偶然发现了两个开源项目。CINT,一个 C/C++ 脚本引擎,以及 Scintilla,一个多语言编辑器,具有语法高亮等功能。
所以这里的可能性显而易见。一个简单的应用程序,可以实现 C/C++ 脚本的快速输入、执行和调试。对于像我这样的开发者来说,这是一个多么方便的工具。我想这也会引起任何学习 C/C++ 语言的人的兴趣。
应用程序目标
这里有巨大的功能机会。但我决定尽量保持功能集最小,以便专注于 CINT 引擎和 Scintilla 编辑器的集成。
提供的功能将使您能够在编辑器中输入 C/C++ 代码,执行它,然后程序会突出显示您对数字世界造成的无数语法错误。
此应用程序还静态链接了这两个项目,我将介绍实现这一目标所需的详细信息。通常对于这样的小型应用程序,你知道,没有完整的安装程序,最好静态链接以避免让用户因缺少 DLL 而感到沮丧。我个人偏好静态链接,如果可能的话,除非我有一个能够验证安装完整性的安装程序。如今,磁盘空间比时间便宜。
我们的项目
该项目是使用 VC6 创建的 MFC 对话框应用程序。我避免使用 MFC 特定的类来集成 CINT 或 Scintilla,因此您应该很容易将此代码移植到非 MFC 项目。
链接到 Scintilla
来自 Scintilla 网站 - Scintilla 是一个编辑器控件,支持语法样式、错误指示器、代码补全和调用提示。选择边距可以包含调试器中使用的标记,例如表示断点和当前行的标记。样式选择比许多编辑器更开放,允许使用比例字体、粗体和斜体、多个前景和背景颜色以及多种字体。
我已将 Scintilla 下载并解压缩到我们项目文件夹的子文件夹 scintilla 中。我们在子文件夹 ./scintilla/vcbuild 中找到 VC 项目文件。在将 SciLexer.dsp 项目添加到我们的工作区后,我们发现它构建没有错误。太好了!
默认情况下,Scintilla 编译为 DLL。我们希望静态链接,因此我们将添加一个 **链接器响应文件** 来创建一个静态库。我创建了两个文件,一个用于发布版本,另一个用于调试版本。
链接器响应文件 (rsp_scintilla.txt) - 发布版本。
/nologo /subsystem:windows ./Release/*.obj /out:../bin/s_scintilla.lib
链接器响应文件 (rsp_scintillad.txt) - 调试版本。
/nologo /subsystem:windows ./Debug/*.obj /out:../bin/sd_scintilla.lib
现在,我们在发布和调试版本中分别添加一个 **构建后事件**,调用相应的响应文件。
构建后事件 - 发布版本。
link -lib @rsp_scintilla.txt
构建后事件 - 调试版本。
link -lib @rsp_scintillad.txt
构建 Scintilla,您应该会发现文件 sd_scintilla.lib 和 s_scintilla.lib 已在 scintilla/bin 文件夹中创建。这些就是我们将链接的库。
我们需要将 Scintilla 头文件添加到我们的项目中,所以我倾向于在 Stdafx.h 文件中完成此操作,因为 Scintilla 库文件可能不会发生太大变化。因此,在 Stdafx.h 中,让我们添加以下 include...
// Include Scintilla parser
#include "scintilla/include/SciLexer.h"
#include "scintilla/include/Scintilla.h"
这里的最后一步是,我们需要链接到 Scintilla 库文件。打开 Stdafx.cpp 文件并添加以下内容....
#ifdef _DEBUG
# pragma comment( lib, "scintilla/bin/sd_scintilla.lib" )
#else
# pragma comment( lib, "scintilla/bin/s_scintilla.lib" )
#endif
这将根据需要链接到调试或发布版本。您也可以将库文件添加到项目设置中,我更喜欢这种方法。
由于 Scintilla 项目是一个 DLL,而我们现在是静态链接,我们需要重新生成它在 DLL 中会做的启动和关闭过程。您通常可以通过在项目中搜索函数 DllMain()
来找到它。果然,我们在 Scintilla 中找到了 DllMain()
。它注册了编辑器窗口类等。我们将此代码添加到我们的启动代码中。在 InitInstance()
中,添加...
// Initialize the Scintilla
if ( !Scintilla_RegisterClasses( AfxGetApp()->m_hInstance ) )
{ AfxMessageBox( "Scintilla failed to initiailze" );
return FALSE;
} // end if
将 Scintilla 关闭代码添加到 ExitInstance()
...
// Release Scintilla
Scintilla_ReleaseResources();
太好了!我们现在已链接到 Scintilla 库。让我们继续处理 CINT。
链接到 CINT
来自 CINT 网站 - CINT 是一个 C/C++ 解释器,旨在处理 C/C++ 脚本。
CINT 是 ROOT 的一部分。ROOT 具有令人着迷的功能集,集成它也会很有趣。我本可以这样做,只是 ROOT 受 LGPL 保护,作为一名商业开发者,我永远无法在实际项目中使用该作品。Scintilla 和 CINT 受更商业友好许可证的约束。我将来肯定想更新此项目以集成 ROOT。
我已将 CINT 下载并解压缩到我们项目文件夹的子文件夹 cint 中。不幸的是,CINT 的 VC 项目文件实际上是 **MAK** 文件的包装器。因此,我选择创建另一个项目并添加我需要的文件。我在子文件夹 ./cint/libcint 中创建了项目文件。该项目原生创建静态库,因此这里除了编译之外别无他法。
编译时,我遇到了一点小麻烦。两个函数 G__SetGlobalcomp()
和 G__ForceBytecodecompilation()
在 Method.cxx 和 v6_dmystrm.c 中都有定义,并且需要这两个文件才能编译 CINT。v6_dmystrm.c 中的版本是我们想要的,所以我用 #if 0
包围了 Method.cxx 中的这两个函数。我也不得不在 Apiifold.cxx 中这样做。我确信这可能是一个疏忽,因为该项目主要是为 UNIX 设计的。希望这将在稍后版本中得到解决。
尽管有这个小插曲,现在一切都编译得很顺利。让我们通过将以下 include 添加到 Stdafx.h 文件中来将其添加到我们的项目中...
// CINT
#include "cint/G__ci.h"
#include "cint/src/Global.h"
我们还需要链接,所以我们将以下几行添加到 Stdafx.cpp 文件中...
#ifdef _DEBUG
# pragma comment( lib, "cint/libcint/Debug/libcint.lib" )
#else
# pragma comment( lib, "cint/libcint/Release/libcint.lib" )
#endif
创建 Scintilla 编辑器和输出窗口
现在两个库都已链接,我们可以开始实际使用它们的有趣部分了。首先,通过在 OnInitDialog()
中调用 InitialiseEditor()
函数来创建 Scintilla 编辑器。我们需要 C/C++ 关键字列表和颜色方案来完成初始化。我使用了一种我喜欢的颜色方案:来自我多年前学习 C++ 的旧 DOS Borland 编译器的原始 **Twilight** 方案。修改颜色以符合您自己的口味应该很简单。
// C++ keywords
static const char g_cppKeyWords[] =
// Standard
"asm auto bool break case catch char class const "
"const_cast continue default delete do double "
"dynamic_cast else enum explicit extern false finally "
"float for friend goto if inline int long mutable "
"namespace new operator private protected public "
"register reinterpret_cast register return short signed "
"sizeof static static_cast struct switch template "
"this throw true try typedef typeid typename "
"union unsigned using virtual void volatile "
"wchar_t while "
// Extended
"__asm __asume __based __box __cdecl __declspec "
"__delegate delegate depreciated dllexport dllimport "
"event __event __except __fastcall __finally __forceinline "
"__int8 __int16 __int32 __int64 __int128 __interface "
"interface __leave naked noinline __noop noreturn "
"nothrow novtable nullptr safecast __stdcall "
"__try __except __finally __unaligned uuid __uuidof "
"__virtual_inheritance";
/// Scintilla Colors structure
struct SScintillaColors
{ int iItem;
COLORREF rgb;
};
// A few basic colors
const COLORREF black = RGB( 0, 0, 0 );
const COLORREF white = RGB( 255, 255, 255 );
const COLORREF green = RGB( 0, 255, 0 );
const COLORREF red = RGB( 255, 0, 0 );
const COLORREF blue = RGB( 0, 0, 255 );
const COLORREF yellow = RGB( 255, 255, 0 );
const COLORREF magenta = RGB( 255, 0, 255 );
const COLORREF cyan = RGB( 0, 255, 255 );
/// Default color scheme
static SScintillaColors g_rgbSyntaxCpp[] =
{
{ SCE_C_COMMENT, green },
{ SCE_C_COMMENTLINE, green },
{ SCE_C_COMMENTDOC, green },
{ SCE_C_NUMBER, magenta },
{ SCE_C_STRING, yellow },
{ SCE_C_CHARACTER, yellow },
{ SCE_C_UUID, cyan },
{ SCE_C_OPERATOR, red },
{ SCE_C_PREPROCESSOR, cyan },
{ SCE_C_WORD, cyan },
{ -1, 0 }
};
void CCintDlg::InitialiseEditor()
{
// Punt if we already have a window
if ( ::IsWindow( m_hwndEditor ) ) return;
// Create editor window
m_hwndEditor = CreateWindowEx( 0, "Scintilla", "",
WS_CHILD | WS_VISIBLE | WS_TABSTOP |
WS_CLIPCHILDREN,
10, 10, 500, 400,
GetSafeHwnd(), NULL /*(HMENU)GuiID*/,
AfxGetApp()->m_hInstance, NULL );
// Did we get the editor window?
if ( !::IsWindow( m_hwndEditor ) )
{ TRACE( "Unable to create editor window\n" );
return;
} // end if
// CPP lexer
SendEditor( SCI_SETLEXER, SCLEX_CPP );
// Set number of style bits to use
SendEditor( SCI_SETSTYLEBITS, 5 );
// Set tab width
SendEditor( SCI_SETTABWIDTH, 4 );
// Use CPP keywords
SendEditor( SCI_SETKEYWORDS, 0, (LPARAM)g_cppKeyWords );
// Set up the global default style. These attributes
// are used wherever no explicit choices are made.
SetAStyle( STYLE_DEFAULT, white, black, 10, "Courier New" );
// Set caret foreground color
SendEditor( SCI_SETCARETFORE, RGB( 255, 255, 255 ) );
// Set all styles
SendEditor( SCI_STYLECLEARALL );
// Set selection color
SendEditor( SCI_SETSELBACK, TRUE, RGB( 0, 0, 255 ) );
// Set syntax colors
for ( long i = 0; g_rgbSyntaxCpp[ i ].iItem != -1; i++ )
SetAStyle( g_rgbSyntaxCpp[ i ].iItem, g_rgbSyntaxCpp[ i ].rgb );
}
我也将 Scintilla 用于输出窗口。只是为了展示一种不同的方法。我通过 ::SendMessage()
更直接地访问了输出窗口。
void CCintDlg::InitialiseOutput()
{
// Punt if we already have a window
if ( ::IsWindow( m_hwndOutput ) ) return;
// Create editor window
m_hwndOutput = CreateWindowEx( 0, "Scintilla", "",
WS_CHILD | WS_VISIBLE | WS_TABSTOP |
WS_CLIPCHILDREN,
10, 10, 500, 400,
GetSafeHwnd(), NULL /*(HMENU)GuiID*/,
AfxGetApp()->m_hInstance, NULL );
// Did we get the editor window?
if ( !::IsWindow( m_hwndEditor ) )
{ TRACE( "Unable to create editor window\n" );
return;
} // end if
// Set number of style bits to use
::SendMessage( m_hwndOutput, SCI_SETSTYLEBITS, 5, 0L );
// Set tab width
::SendMessage( m_hwndOutput, SCI_SETTABWIDTH, 4, 0L );
// Set foreground color
::SendMessage( m_hwndOutput, SCI_STYLESETFORE,
STYLE_DEFAULT, (LPARAM)RGB( 255, 255, 255 ) );
// Set background color
::SendMessage( m_hwndOutput, SCI_STYLESETBACK, STYLE_DEFAULT, (LPARAM)RGB( 0, 0, 0 ) );
// Set font
::SendMessage( m_hwndOutput, SCI_STYLESETFONT, STYLE_DEFAULT, (LPARAM)"Courier New" );
// Set selection color
::SendMessage( m_hwndOutput, SCI_SETSELBACK, (WPARAM)TRUE, (LPARAM)RGB( 0, 0, 255 ) );
// Set all styles
::SendMessage( m_hwndOutput, SCI_STYLECLEARALL, 0, 0L );
}
为了定位窗口,我使用了以下代码。这很简单,因为我们只是处理普通的窗口句柄...
BOOL CCintDlg::Size()
{
// Ensure valid window
if ( !::IsWindow( GetSafeHwnd() ) )
return FALSE;
// Get window size
RECT rect, ctrl;
GetClientRect( &rect );
CopyRect( &ctrl, &rect );
// Position the editor window
ctrl.bottom -= ( 6 * 24 );
CWnd *pWnd = CWnd::FromHandle( m_hwndEditor );
if ( pWnd ) pWnd->MoveWindow( &ctrl );
// Position the output window
ctrl.top = ctrl.bottom;
ctrl.bottom = rect.bottom;
pWnd = CWnd::FromHandle( m_hwndOutput );
if ( pWnd ) pWnd->MoveWindow( &ctrl );
return TRUE;
}
使用 CINT 执行代码
当然,我们希望能够执行脚本,并将任何生成的输出保存在可复制粘贴的窗口中。CINT 将脚本的输出发送到标准输出流 STDOUT。我们需要拦截此数据。为了简化,我从开源项目 rulib 中借用了 CHookStdio
类(别担心,没有侵犯许可证)。CHookStdio
使我们能够轻松挂钩 STDOUT 并访问数据。请注意,我们必须向 CHookStdio
传递一个参数,指示我们需要多少缓冲区空间。在编写脚本时要注意此大小。我将其设置为 64K。
因此,现在执行脚本的步骤是...
- 从 Scintilla 获取文本
- 挂钩 STDOUT
- 发送到 CINT 进行处理
- 检查 CINT 错误
- 将挂钩的 STDOUT 数据写入输出窗口
以下是详细信息。
void CCintDlg::OnExecute()
{
// Reset CINT
G__scratch_all();
g_sCintLastError = "";
// Reset Scintilla
SendEditor( SCI_MARKERDELETEALL, 0 );
// Clear output window
::SendMessage( m_hwndOutput, SCI_SETTEXT, 0, (WPARAM)"" );
// Get the editor window handle
CWnd *pWnd = CWnd::FromHandle( m_hwndEditor );
if ( !pWnd ) return;
// Get the script text
CString strScript;
pWnd->GetWindowText( strScript );
if ( strScript.IsEmpty() ) return;
// Must add to get proper line number
strScript = "#line 0\r\n" + strScript;
// Hook stdio output
CHookStdio hs( STD_OUTPUT_HANDLE );
// Set error callback function
G__set_errmsgcallback( &CCintDlg::CintError );
// Execute the program
if ( !G__int( G__exec_text( (LPCTSTR)strScript ) ) )
{
// Initilaize error markers
SendEditor( SCI_MARKERDEFINE, 0, SC_MARK_SHORTARROW );
SendEditor( SCI_MARKERSETFORE, 0, RGB( 80, 0, 0 ) );
SendEditor( SCI_MARKERSETBACK, 0, RGB( 255, 0, 0 ) );
// Set error marker to proper line
int nErrLine = G__lasterror_linenum();
SendEditor( SCI_MARKERADD, nErrLine - 1, 0 );
// Show the error string
ShowError( g_sCintLastError.c_str() );
return;
} // end if
// Set foreground color
::SendMessage( m_hwndOutput, SCI_STYLESETFORE,
STYLE_DEFAULT, (LPARAM)RGB( 255, 255, 255 ) );
::SendMessage( m_hwndOutput, SCI_STYLECLEARALL, 0, 0L );
// Get output
char buf[ 64 * 1024 ] = "";
buf[ hs.Read( buf, sizeof( buf ) - 1 ) ] = 0;
// Show script output
if ( *buf ) ::SendMessage( m_hwndOutput, SCI_SETTEXT, 0, (WPARAM)buf );
}
此函数还包括高亮显示脚本中错误的 [代码]。[错误] 看起来是这样的。
结论
就这样。所以玩得开心。
我可以想到这个项目的一个实际用途是生成代码优化时经常出现的乏味的查找表。现在我可以创建简单的脚本并在需要时运行它,而不是浪费时间在一个完整的项目上来生成一个简单的表。(尽管我不得不承认最近我用 PHP 来做这件事。)
这是一个生成平方根查找表的脚本...
// Start the table
printf( "const double g_rdLookup_Sqrt[] = \r\n{\r\n\t" );
// Generate items
for ( int i = 0; i < 100; i++ )
{
// Add value separator
if ( i ) printf( ", " );
// Break into reasonable pieces
if ( i && !( i % 8 ) ) printf( "\r\n\t" );
// Calculate value
printf( "%g", (double)sqrt( (double)i ) );
}
// Complete the table
printf( "\r\n};" );
这是输出...
const double g_rdLookup_Sqrt[] =
{
0, 1, 1.41421, 1.73205, 2, 2.23607, 2.44949, 2.64575,
2.82843, 3, 3.16228, 3.31662, 3.4641, 3.60555, 3.74166, 3.87298,
4, 4.12311, 4.24264, 4.3589, 4.47214, 4.58258, 4.69042, 4.79583,
4.89898, 5, 5.09902, 5.19615, 5.2915, 5.38516, 5.47723, 5.56776,
5.65685, 5.74456, 5.83095, 5.91608, 6, 6.08276, 6.16441, 6.245,
6.32456, 6.40312, 6.48074, 6.55744, 6.63325, 6.7082, 6.78233, 6.85565,
6.9282, 7, 7.07107, 7.14143, 7.2111, 7.28011, 7.34847, 7.4162,
7.48331, 7.54983, 7.61577, 7.68115, 7.74597, 7.81025, 7.87401, 7.93725,
8, 8.06226, 8.12404, 8.18535, 8.24621, 8.30662, 8.3666, 8.42615,
8.48528, 8.544, 8.60233, 8.66025, 8.7178, 8.77496, 8.83176, 8.88819,
8.94427, 9, 9.05539, 9.11043, 9.16515, 9.21954, 9.27362, 9.32738,
9.38083, 9.43398, 9.48683, 9.53939, 9.59166, 9.64365, 9.69536, 9.74679,
9.79796, 9.84886, 9.89949, 9.94987
};