语言如何自我扩展?[关闭]


208

我正在学习C ++,并且刚刚开始学习Qt的一些功能来编写GUI程序。我问自己以下问题:

以前没有语法能够向操作系统询问窗口或通过网络进行通信的方法(我承认我也不完全理解的API)的C ++突然如何通过用C ++自己编写的库获得这种功能对我来说,一切似乎都是圆圆的。您可能在这些库中提出了哪些C ++指令?

我意识到对于一个经验丰富的软件开发人员来说,这个问题似乎微不足道,但是我已经进行了数小时的研究,没有找到任何直接的答案。到了我无法理解有关Qt的教程的地步,因为库的存在对我来说是不可理解的。


27
std :: cout怎么在监视器上绘制任何东西?还是坐在了解您硬件的编译器之上?
doctorlove

14
好问题。最终,在学习硬件之前很难回答。
user541686 2014年

17
Qt不是该语言的扩展(这需要Qt感知的编译器)。它只是添加到您的武器中的一个。最终,所有库在最低级别上都通过系统调用与操作系统进行通信,该系统调用与语言无关,但是在很大程度上取决于操作系统和CPU架构。
DevSolar 2014年

8
afaik,C ++具有内联汇编,可以执行几乎所有操作
显示名称

9
@DevSolar:实际上,Qt确实通过其自身的信号槽机制,反射和许多其他动态功能来扩展语言。那些事情需要编译器(元对象编译器)才能编译为C ++代码。
任思远2014年

Answers:


194

一种计算机像一个洋葱,它有许多许多层,从纯硬件的内芯到最外面的应用层。每一层将其自身的部分暴露给下一个外层,以便外层可以使用某些内层功能。

在Windows的情况下,操作系统为Windows上运行的应用程序公开所谓的WIN32 API。Qt库使用该API向其自己的API提供使用Qt的应用程序。您使用Qt,Qt使用WIN32,WIN32使用较低级别的Windows操作系统,依此类推,直到硬件中出现电信号为止。


56
注意:Qt这里提供其下一层的抽象,因为在Linux上Qt调用Linux API而不是WIN32 API。
Matthieu M.

5
我可能会在Qt的示例上做更多的阐述,就像它毫不费力地扩展c ++功能一样。当现实发生时,他们付出了很多努力来制作一个通用的API,(有争议的)许多不同的“洋葱核”。它们是在非便携式非标准后端之上提供可移植性的工具。
2014年

81
计算机就像洋葱:切开洋葱会使您哭泣,但之后却有点好吃。
alecov

3
@ChristopherPfohl是的,必须使用它,因为我无法弄清一台计算机像一盒巧克力。:)
一些程序员哥们2014年

1
@Celeritas老师可能说过了user32.dll,或者可能gdi32.dll
user253751

59

没错,一般来说,库无法使不可能实现的一切变为可能。

但是不必为了使用C ++程序就可以用C ++编写这些库。即使它们是用C ++编写的,它们也可能在内部使用其他非C ++编写的库。因此,只要有某种方法可以在C ++之外执行,C ++不会提供任何方法来阻止添加它。

在相当低的水平上,某些C ++(或C)调用的函数将用汇编语言编写,并且该汇编包含必需的指令,以执行C ++中不可能(或不容易)的事情,例如调用系统功能。到那时,该系统调用可以执行您的计算机能够执行的所有操作,仅仅是因为没有什么可以停止它。


您是说那些用其他语言编写的库已经被其他编译器编译过了吗?然后,必须有某种接口文件,将库提供给C ++的每个函数调用链接到库的预编译版本?因此,允许C ++编译器知道将这些调用转换为什么?
Med Larbi Sentissi 2014年

2
@MedLarbiSentissi 1)不一定是其他编译器。一个单一的编译器有可能(而且经常是这种情况)能够编译包括汇编语言在内的多种语言,甚至可以通过内联汇编语言编译C ++。2)取决于特定的系统和编译器,确实可以使用某种接口文件来完成从C ++可以调用这些函数的操作,但是那种接口文件可能已经是可以直接从C ++使用的C(甚至C ++)头文件。

1
@MedLarbiSentissi:许多Windows库都被编译为dll文件,该dll文件包含其自己的界面以及代码。您可以查看dll,并查看它可以使用的功能列表。它们通常还带有一个C头文件。创建exe时,它包含需要运行的dll列表。当操作系统尝试加载您的exe时,它还将在开始执行之前自动加载这些dll。
Mooing Duck

8
这个答案似乎暗示“魔术”完全是在其他被调用的语言中实现的,但是实际上,构成大多数现代操作系统的大多数代码是C(仅用汇编编写的非常硬件绑定或对性能至关重要的部件)- 绝对有可能使用C ++代替。关键是,没有“魔术”,而是创建了语言来构建如此强大的抽象,一旦您可以与硬件交互,可能性几乎是无限的。
Matteo Italia

1
@hvd我认为此讨论中的整个冲突是您(和其他人)将C定义为其指定的功能。实际上,编译器所添加的内容远远超出了所指定的内容,这使C的问题很难回答。对我来说,关于语言的特殊之处(因此就是它的本质)是表达程序流程和结构可能性的元方式。结构化的元素对此并不重要,因为它只是更好的ASM代码,编译器可以根据需要添加它们
LionC 2014年

43

C和C ++具有2个属性,这些属性允许OP谈论所有这些扩展性。

  1. C和C ++可以访问内存
  2. C和C ++可以为非C或C ++语言的指令调用汇编代码。

在内核或基本的非保护模式平台中,外设(例如串行端口或磁盘驱动器)以与RAM相同的方式映射到内存映射中。内存是一系列开关,翻转外围设备的开关(例如串行端口或磁盘驱动器)可使外围设备执行有用的操作。

在受保护模式的操作系统中,如果要从用户空间访问内核(例如,在写入文件系统或在屏幕上绘制像素时),则需要进行系统调用。C没有指令进行系统调用,但是C可以调用可以触发正确系统调用的汇编代码,这就是允许C语言与内核对话的原因。

为了使对特定平台的编程更容易,系统调用被包装在更复杂的函数中,这些函数可以在自己的程序中执行某些有用的功能。一个是免费的,可以直接调用系统调用(使用汇编程序),但是仅使用平台提供的包装器功能之一可能会更容易。

API的另一个层次比系统调用有用得多。以malloc为例。这不仅会调用系统获取大容量的内存,而且还会通过记录所有发生的事情来管理此内存。

Win32 API通过公共平台窗口小部件集包装了一些图形功能。Qt通过跨平台方式包装Win32(或X Windows)API来进一步扩展这一功能。

尽管从根本上说,C编译器将C代码转换为机器代码,并且由于计算机被设计为使用机器代码,所以您应该期望C能够完成最大的任务或计算机可以做什么。包装器库所做的全部工作就是为您完成繁重的工作,因此您不必这样做。


关于#2的警告:C和C ++只能可行地调用符合编译器理解和期望的“调用约定”的函数。(汇编代码可以使用它喜欢的任何约定,甚至可以根本不使用任何约定-因此该代码可能无法直接调用。)幸运的是,每个自重的编译器都提供了一种使用平台的公共约定的内置方法。(例如,Windows C编译器允许您拥有/使用使用“ cdecl”,“ stdcall”或“ fastcall”约定的函数。)但是汇编代码必须使用编译器知道的约定,否则C和C ++可以使用不能直接调用。
cHao 2014年

2
另外:内存映射的I / O很常见,但并非全部。例如,PC通常使用x86的“ I / O端口”(一种完全不同的机制)寻址串行端口,磁盘驱动器等。(视频缓冲区通常是内存映射的,但是视频模式等通常是通过I / O端口控制的。)
cHao 2014年

@cHao:当然,使用INP和OUTP的经典方法已逐渐被DMA支持;PCI代似乎可以使用内存映射的特殊功能寄存器来做更多的事情,现在有了一种方法可以自动将设备映射到非重叠区域并从驱动程序中发现它们,而使用I / O端口则更少。
Ben Voigt 2014年

现代外围设备将使用DMA进行批量数据传输,但您仍将使用可寻址内存对DMA控制器进行编程
doron 2014年

@doron:嗯,至少在您理智的情况下,可以通过PC上的I / O地址空间(而不是内存空间)对DMA控制器进行编程。现代x86 CPU喜欢对内存访问进行重新排序以提高性能。使用MMIO,这可能是灾难性的……因此,您需要小心使这些地址变为非缓存状态并将序列化指令放在所有正确的位置。OTOH,x86本身可确保对I / O空间的读写按程序顺序进行。这就是为什么许多重要的东西仍然通过I / O空间(通常不能通过指针访问)完成的原因,并且可能总是如此。
cHao 2014年

23

语言(如C ++ 11)是规范,通常在纸上用英语书写。查看最新的C ++ 11草案(或从ISO供应商处购买价格昂贵的最终规范)。

通常,您使用具有某种语言实现的计算机(原则上,您可以在没有任何计算机的情况下运行C ++程序,例如使用一群人类奴隶来解释它;这是不道德且低效的)

您的C ++实现一般可以在某些操作系统上运行并与之通信(通常使用某些系统库中的某些实现特定的代码)。通常,通信是通过系统调用完成的。在syscalls(2)中查找实例,以获取Linux内核上可用的系统调用列表。

从应用程序的角度来看,系统调用是基本的机器指令,例如SYSENTERx86-64上的某些约定(ABI

在我的Linux桌面上,Qt库位于X11客户端库之上,该客户端库通过X Windows协议与X11服务器Xorg 通信

在Linux上,ldd在可执行文件上使用以查看库的依赖性列表(较长)。pmap在运行的进程上使用,以查看在运行时“加载”了哪些进程。顺便说一句,在Linux上,您的应用程序可能仅使用免费软件,您可以研究其源代码(从Qt到Xlib,libc等),以了解更多信息。


2
作为参考,ANSI出售C ++ 11规范的价格略低一些,仅为60美元。(它曾经是通行证的一半,但实际上是通货膨胀。:P)它被标记为INCITS / ISO / IEC 14882,但至少与ISO提供的基本规格相同。不确定勘误/ TR。
cHao 2014年

19

我认为您所缺少的概念是系统调用。每个操作系统都提供了大量资源和功能,您可以利用这些资源和功能来执行与底层操作系统相关的事情。即使调用常规库函数,它也可能在后台进行系统调用。

系统调用是利用操作系统功能的一种低级方法,但是使用起来可能很复杂且繁琐,因此通常将它们“包装”在API中,这样您就不必直接处理它们。但是在底层,几乎所有与操作系统相关的资源都将使用系统调用,包括打印,联网和套接字等。

对于Windows,Microsoft Windows的GUI实际上已写入内核,因此存在用于制作Windows,绘制图形等的系统调用。在其他操作系统中,GUI可能不是内核的一部分,在这种情况下据我所知,不会有任何与GUI相关的系统调用,并且您只能在更低的级别上使用任何可用的低级图形和输入相关调用。


3
缺少的重要一点是,这些系统调用绝不是魔术。它们由通常使用C(++)编写的内核提供服务。而且,系统调用甚至不是必需的。在没有内存保护的基本OS中,可以通过将像素直接放入硬件帧缓冲区来绘制窗口。
el.pescado 2014年

15

好问题。每个新的C或C ++开发人员都牢记这一点。在本文的其余部分,我假设使用标准的x86机器。如果您使用的是Microsoft C ++编译器,请打开记事本并键入(将文件命名为Test.c)。

int main(int argc, char **argv)
{
   return 0
}

然后编译该文件(使用开发人员命令提示符)cl Test.c /FaTest.asm

现在,在记事本中打开Test.asm。您所看到的是已翻译的代码-C / C ++已被翻译为汇编器。你有提示吗?

_main   PROC
    push    ebp
    mov ebp, esp
    xor eax, eax
    pop ebp
    ret 0
_main   ENDP

C / C ++程序旨在在金属上运行。这意味着他们可以访问较低级别的硬件,从而更容易利用硬件的功能。说,我要在x86机器上编写C库getch()。

根据汇编程序,我将以这种方式键入一些内容:

_getch proc 
   xor AH, AH
   int 16h
   ;AL contains the keycode (AX is already there - so just return)
ret

我用汇编器运行它并生成一个.OBJ-将其命名为getch.obj。

然后,我编写一个C程序(我不包含任何内容)

extern char getch();

void main(int, char **)
{
  getch();
}

现在命名该文件-GetChTest.c。一起传递getch.obj来编译此文件。(或单独编译为.obj和LINK GetChTest.Obj和getch.Obj一起生成GetChTest.exe)。

运行GetChTest.exe,您会发现它正在等待键盘输入。

C / C ++编程不仅与语言有关。要成为一名优秀的C / C ++程序员,您需要对所运行机器的类型有充分的了解。您将需要知道内存管理的处理方式,寄存器的结构等。您可能不需要所有这些信息即可进行常规编程-但它们会极大地帮助您。除了基本的硬件知识之外,如果您了解编译器的工作原理(即编译器的工作方式),也肯定会有所帮助-这可以使您根据需要调整代码。这是一个有趣的包!

两种语言都支持__asm关键字,这意味着您也可以混合使用汇编语言代码。学习C和C ++将使您总体上成为一个更全面的程序员。

不必始终与汇编程序链接。我之所以提到它,是因为我认为这可以帮助您更好地理解。通常,大多数此类库调用都使用操作系统提供的系统调用/ API(操作系统依次进行硬件交互)。


10

C ++如何突然通过自己用C ++编写的库获得这些功能?

使用其他库没有什么神奇的。库是您可以调用的简单的大型函数包。

考虑自己编写这样的函数

void addExclamation(std::string &str)
{
    str.push_back('!');
}

现在,如果包含该文件,则可以编写addExclamation(myVeryOwnString);。现在您可能会问:“ C ++是如何突然获得了将感叹号添加到字符串的功能的?” 答案很简单:您编写了一个函数来执行该操作,然后调用它。

因此,要回答有关C ++如何获得通过C ++编写的库绘制窗口的功能的问题,答案是相同的。其他人编写了函数来执行此操作,然后将其编译并以库的形式提供给您。

其他问题回答了窗口绘图的实际工作方式,但是您对库的工作方式感到困惑,因此我想解决您问题中最基本的部分。


8

关键在于操作系统是否可以公开API以及有关如何使用此API的详细说明。

操作系统提供了一组带有调用约定的API。调用约定定义了将参数赋予API的方式以及如何返回结果以及如何执行实际调用。

操作系统和为其创建代码的编译器可以很好地协同工作,因此通常不必考虑它,而只需使用它。


7

不需要用于创建窗口的特殊语法。所需要的只是操作系统提供了一个用于创建窗口的API。这种API由C ++确实提供语法的简单函数调用组成。

而且,C和C ++被称为系统编程语言,并且能够访问任意指针(硬件可能会将其映射到某些设备)。此外,调用汇编中定义的函数也非常简单,这允许处理器提供全部操作范围。因此,可以使用C或C ++和少量汇编来编写OS本身。

还应该提到,Qt是一个不好的例子,因为它使用了所谓的元编译器来扩展 C ++的语法。但是,这与调用操作系统提供的API实际绘制或创建窗口的能力无关。


7

首先,我认为有些误解

C ++以前没有语法能够向操作系统询问窗口或通过网络进行通信的方式,该怎么办?

没有用于执行OS操作的语法。这是语义问题。

通过用C ++自己编写的库突然获得了这种功能

好吧,操作系统主要是用C编写的。您可以使用共享库(so,dll)来调用外部代码。此外,操作系统代码可以在syscalls *或您可以使用assembly调用的中断上注册系统例程。共享库通常只是让系统调用您,因此可以使用内联程序集来避免使用。

这是关于 它的不错的教程:http : //www.win.tue.nl/~aeb/linux/lk/lk-4.html
它适用于Linux,但是原理是相同的。

操作系统如何在图形卡,网卡等上进行操作?这是一个非常广泛的主题,但是大多数情况下,您需要访问中断,端口或将某些数据写入特殊的内存区域。由于该操作受到保护,因此无论如何您都需要通过操作系统来调用它们。


7

为了提供与其他答案略有不同的观点,我将这样回答。

(免责声明:我只是在简化一些事情,我给出的情况纯粹是假设的,其目的是用来展示概念,而不是100%真实地生活)。

从另一个角度考虑问题,想象一下您刚刚编写了一个具有基本线程,窗口和内存管理功能的简单操作系统。您想实现一个C ++库,以允许用户使用C ++进行编程,并执行诸如制作窗口,在Windows上绘制等操作。问题是,如何执行此操作。

首先,由于C ++可以编译为机器代码,因此您需要定义一种使用机器代码与C ++进行接口的方法。这是函数进入的地方,函数接受参数并提供返回值,因此它们提供了在代码的不同部分之间传输数据的标准方法。他们通过建立称为调用约定的方式来做到这一点。

一个调用约定状态,其中和参数应如何放置在内存中,这样当它被执行的功能可以找到他们。当一个函数被调用时,调用函数将参数放入内存中,然后要求CPU跳转到另一个函数,在跳转回被调用的位置之前,它会执行该操作。这意味着被调用的代码绝对可以是任何东西,并且不会改变函数的调用方式。但是,在这种情况下,该功能背后的代码将与操作系统有关,并且将在操作系统的内部状态下运行。

因此,几个月后,您就将所有OS功能都整理好了。您的用户可以调用函数来创建窗口并在其上绘图,它们可以创建线程和各种奇妙的事物。但是,这就是问题所在,您的OS的功能将与Linux的功能或Windows的功能不同。因此,您决定需要为用户提供一个标准界面,以便他们可以编写可移植的代码。QT来了。

您几乎可以肯定知道,QT包含许多有用的类和函数,它们可以执行操作系统所要执行的各种操作,但其方式与底层操作系统无关。这种工作方式是QT提供的类和函数在用户看来是一致的,但是对于每个操作系统,这些函数背后的代码都是不同的。例如,QT的QApplication :: closeAllWindows()实际上将根据所使用的版本来调用每个操作系统的专用窗口关闭功能。在Windows中,很可能会调用CloseWindow(hwnd),而在使用X窗口系统的操作系统上,可能会调用XDestroyWindow(display,window)。

显而易见,一个操作系统具有许多层,所有这些层都必须通过许多种类的接口进行交互。我什至没有涉及很多方面,但是要解释它们都需要很长时间。如果您对操作系统的内部运作进一步感兴趣,建议您查看OS dev wiki

请记住,尽管许多操作系统选择将接口公开给C / C ++的原因是它们可以编译为机器代码,它们允许将汇编指令与自己的代码混合在一起,并且为程序员提供了极大的自由度。

同样,这里发生了很多事情。我想继续说明为什么.so和.dll文件之类的库不必用C / C ++编写,而可以用汇编语言或其他语言编写,但是我觉得如果再添加的话,我也可以写了整篇文章,尽管我很想做,但我没有网站来托管它。


6

当您尝试在屏幕上绘制某些内容时,您的代码会调用其他代码,这些代码又会调用其他代码(等),直到最后出现“系统调用”,这是CPU可以执行的特殊指令。这些指令可以用汇编语言编写,也可以用C ++编写,前提是编译器支持它们的“本能”(这些功能是编译器通过将它们转换成CPU可以理解的特殊代码来“专门”处理的功能)。他们的工作是告诉操作系统做某事。

当发生系统调用时,将调用一个函数,该函数将调用另一个函数(等),直到最终告诉显示驱动程序在屏幕上绘制某些内容。此时,显示驱动程序将查看物理内存中的特定区域,该区域实际上不是内存,而是一个可以写入的地址范围,就好像它是内存一样。但是,写入该地址范围会导致图形硬件截取内存写入,并在屏幕上绘制一些内容。可以使用C ++
编写对该内存区域的写操作,因为在软件方面,这只是常规的内存访问。只是硬件处理方式有所不同。
因此,这是它如何工作的非常基本的解释。


4
Afaik的systemcall并不是真正的cpu指令,与内部函数无关。它更多地是与设备通信的操作系统内核的功能。
MatthiasB 2014年

3
@MatthiasB:嗯,您错了,因为syscall(及其表亲sysenter)确实是CPU指令。
user541686 2014年

2
这只是改善您答案的提示,因为我自己还不清楚。不要将其视为人身攻击或其他任何攻击。
MatthiasB 2014年

1
@MatthiasB:我不是个人。我说的是我已经知道答案实际上并不是100%准确,但是我认为这是回答OP的足够简单的方法-因此,如果您确实知道可以写出更好答案的方法,请您自己编写回答或花时间编辑我的。我确实没有什么值得补充的东西,因此,如果您希望在此页面上看到更好的东西,就必须自己付出努力。
user541686

3
系统调用使用软件中断来完成。诸如此类sysenter的指令是优化的调用路径,因为中断处理程序使用的上下文切换并没有所有人所希望的那样快,但是从根本上讲,它仍然是由软件生成的中断,而通过矢量处理由OS内核安装的处理程序进行处理。通过执行的上下文切换过程的一部分sysenter是更改处理器中的模式位以将环0设置为-完全访问所有特权指令,寄存器以及内存和I / O区域。
Ben Voigt 2014年

4

您的C ++程序正在使用Qt库(也用C ++编码)。Qt库将使用Windows CreateWindowEx函数(在kernel32.dll 中用C编码)。或者在Linux下,它可能正在使用Xlib(也用C编码),但是它也可能发送X协议中表示“ 请为我创建一个窗口 ”的原始字节。

与您的catch 22问题有关的是一个历史记录,即“第一个C ++编译器是用C ++编写的”,尽管实际上它是一个带有一些C ++概念的C编译器,足以编译第一个版本,然后可以自行编译。

同样,GCC编译器使用GCC扩展名:首先将其编译为一个版本,然后用于重新编译自身。(GCC构建说明)


2

我如何看待这个问题,实际上是一个编译器问题。

以这种方式查看,您可以在Assembly中编写一段代码(可以用任何一种语言来完成),它将您要调用Z ++的新编写的语言转换为Assembly,为简单起见,可以将其称为编译器(它是编译器) 。

现在,为该编译器提供一些基本功能,以便您可以编写int,字符串,数组等。实际上,您为其提供了足够的功能,以便可以使用Z ++编写编译器本身。现在您有了一个用Z ++编写的Z ++编译器,非常简洁。

更酷的是,现在您可以使用已具备的功能向该编译器添加功能,从而通过使用以前的功能来扩展具有新功能的Z ++语言

例如,如果您编写了足够的代码来绘制任何颜色的像素,则可以使用Z ++对其进行扩展以绘制所需的任何内容。


0

硬件可以使这种情况发生。您可以将图形内存视为一个大阵列(由屏幕上的每个像素组成)。要绘制到屏幕上,您可以使用C ++或允许直接访问该内存的任何语言写入此内存。该内存恰好可以被图形卡访问或位于图形卡上。

在现代系统上,由于各种限制,直接访问图形内存将需要编写驱动程序,因此您可以使用间接方式。库会创建一个窗口(实际上只是一个图像,就像其他任何图像一样),然后将该图像写入图形内存,然后GPU将其显示在屏幕上。除了能够写入特定内存位置(指针就是用于此目的)之外,无需在语言中添加任何内容。


我要提出的观点是,某种语言不需要“扩展”自身,就需要重写该语言的新版本,并且因为要做任何有趣的程序,它并不是真正的循环式必须与硬件接口。
约翰
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.